Introducción: SOLID en el mundo de Rust
Los principios SOLID son la base de una arquitectura flexible y mantenible. Sin embargo, su implementación en Rust tiene sus propias particularidades, debido a su sistema único de propiedad, los traits y la ausencia de POO clásica. En este artículo, analizaremos cómo aplicar cada uno de los cinco principios a nivel avanzado, utilizando Rust idiomático. No solo enumeraremos las reglas, sino que mostraremos cómo se transforman en el contexto del sistema de tipos y el préstamo.
Este artículo está dirigido a desarrolladores que ya conocen los fundamentos de Rust y SOLID. Nos centraremos en los detalles: cómo evitar violar LSP al trabajar con tipos genéricos, cómo ISP afecta el diseño de traits, y por qué DIP en Rust a menudo requiere el uso de objetos trait o Box.
1. Principio de Responsabilidad Única (SRP) y Propiedad
En Rust, el SRP adquiere una dimensión adicional debido a las estrictas reglas de propiedad. Una clase (o estructura) debe tener una sola razón para cambiar. Pero, ¿cómo se relaciona esto con la transferencia de propiedad?
Problema: estructura que gestiona recursos y lógica
Consideremos un ejemplo donde una estructura gestiona simultáneamente un descriptor de archivo y analiza datos. Esto viola el SRP.
use std::fs::File;use std::io::Read;
// ❌ Violación de SRP: la estructura gestiona el recurso y procesa los datosstruct DataProcessor { file: File,}
impl DataProcessor { fn new(path: &str) -> Self { DataProcessor { file: File::open(path).unwrap(), } }
fn process(&mut self) -> Vec<u8> { let mut buffer = Vec::new(); self.file.read_to_end(&mut buffer).unwrap(); // ... lógica de procesamiento compleja buffer }}Solución: separación mediante transferencia de propiedad
Separamos las responsabilidades: una estructura se encarga de la lectura, otra del procesamiento. La propiedad se transfiere explícitamente.
use std::fs::File;use std::io::Read;
// ✅ Primera responsabilidad: gestión del archivostruct FileReader { file: File,}
impl FileReader { fn new(path: &str) -> Self { FileReader { file: File::open(path).unwrap(), } }
fn read(self) -> Vec<u8> { let mut buffer = Vec::new(); // self.file se consume (moved), ya que después de la lectura no es necesaria self.file.read_to_end(&mut buffer).unwrap(); buffer }}
// ✅ Segunda responsabilidad: procesamiento de datosstruct DataProcessor { data: Vec<u8>,}
impl DataProcessor { fn new(data: Vec<u8>) -> Self { DataProcessor { data } }
fn process(&self) -> Vec<u8> { // Procesamiento inmutable self.data.iter().map(|&b| b + 1).collect() }}
fn main() { let reader = FileReader::new("data.bin"); let raw_data = reader.read(); // reader ya no está disponible let processor = DataProcessor::new(raw_data); let result = processor.process();}Punto clave: usamos self (propiedad) en lugar de &self en read() para indicar explícitamente que después de la lectura, FileReader ya no es necesario. Esto hace que el flujo de datos sea transparente y previene fugas de recursos.
2. Principio de Abierto/Cerrado (OCP) y Traits
El OCP establece: las entidades deben estar abiertas para la extensión, pero cerradas para la modificación. En Rust, esto se implementa idealmente mediante traits y genéricos sin usar tablas virtuales (vtable), a menos que sea necesario.
Ejemplo avanzado: estrategia con cero sobrecarga
Supongamos que tenemos un sistema de registro. En lugar de modificar el núcleo de la aplicación para cada nuevo backend, usamos polimorfismo a través de traits.
/