Principios SOLID en Rust: teoría y práctica para desarrolladores

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course
```html

Introducción: Principios SOLID en Rust — ¿por qué son necesarios?

SOLID son cinco principios fundamentales de la programación orientada a objetos que ayudan a crear código flexible, mantenible y escalable. Aunque Rust no es un lenguaje OOP clásico (no tiene herencia de clases en el sentido tradicional), su sistema de ownership, traits y genéricos permite implementar brillantemente todos los principios SOLID. Comprender estos principios en el contexto de Rust ayuda a escribir código que sea fácil de probar, refactorizar y extender.

En este artículo analizaremos cada uno de los cinco principios SOLID con ejemplos prácticos en Rust. Verás cómo trait reemplaza las interfaces, cómo Box<dyn Trait> ayuda con la inversión de dependencias y cómo evitar errores típicos.



1. Principio de Responsabilidad Única (SRP) en Rust

Single Responsibility Principle (SRP): cada módulo, estructura o función debe tener solo una razón para cambiar. En Rust, esto significa que una estructura debe ser responsable de una sola entidad, sin mezclar lógica de negocio, manejo de datos y salida.



Ejemplo de violación de SRP

struct User {    name: String,    email: String,}

impl User { // Mezclamos lógica de negocio y persistencia fn save_to_db(&self) { println!("Guardando {} en la BD", self.name); } fn send_email(&self, message: &str) { println!("Enviando email a {}: {}", self.email, message); }}

Aquí la estructura User es responsable tanto de los datos del usuario como de guardar y enviar correos. Esto viola SRP.



Implementación correcta de SRP

struct User {    name: String,    email: String,}

struct UserRepository;impl UserRepository { fn save(&self, user: &User) { println!("Guardando {} en la BD", user.name); }}

struct EmailService;impl EmailService { fn send(&self, user: &User, message: &str) { println!("Enviando email a {}: {}", user.email, message); }}

Ahora cada estructura tiene una única responsabilidad: User — datos, UserRepository — almacenamiento, EmailService — envío. El código se ha vuelto más modular y testeable.



2. Principio de Abierto/Cerrado (OCP) mediante traits

Open/Closed Principle (OCP): las entidades de software deben estar abiertas para extensión, pero cerradas para modificación. En Rust, esto se implementa idealmente con trait y despacho dinámico.



Ejemplo sin OCP

fn calculate_area(shape: &str, params: &[f64]) -> f64 {    match shape {        "circle" => std::f64::consts::PI * params[0] * params[0],        "rectangle" => params[0] * params[1],        _ => panic!("Figura desconocida"),    }}

Al agregar una nueva figura (por ejemplo, triángulo), habría que modificar la función.



Enfoque correcto con OCP

trait Shape {    fn area(&self) -> f64;}

struct Circle { radius: f64 }impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }}

struct Rectangle { width: f64, height: f64 }impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height }}

// Función cerrada para modificación, pero abierta para extensiónfn print_area(shape: &dyn Shape) { println!("Área: {}", shape.area());}

Ahora para agregar un triángulo basta con crear una nueva estructura e implementar trait Shape. La función print_area no requiere cambios.



3. Principio de Sustitución de Liskov (LSP) y herencia mediante traits

Liskov Substitution Principle (LSP): los objetos en un programa deben ser reemplazables por instancias de sus subtipos sin alterar la corrección del programa. En Rust, esto se logra mediante traits y subtipado estructural.



Ejemplo de violación de LSP

trait Bird {    fn fly(&self);}

struct Sparrow;impl Bird for Sparrow { fn fly(&self) { println!("El gorrión vuela"); }}

struct Penguin;impl Bird for Penguin { fn fly(&self) { panic!("¡Los pingüinos no vuelan!"); }}

Aquí Penguin viola LSP porque no puede sustituir correctamente a Bird sin causar errores.



Implementación correcta de LSP

trait Bird {    fn make_sound(&self);}

trait Flyable: Bird { fn fly(&self);}

struct Sparrow;impl Bird for Sparrow { fn make_sound(&self) { println!("Pío pío"); }}impl Flyable for Sparrow { fn fly(&self) { println!("El gorrión vuela"); }}

struct Penguin;impl Bird for Penguin { fn make_sound(&self) { println!("Honk honk"); }}// Penguin no implementa Flyable, lo cual es correcto

Ahora cada tipo implementa solo los comportamientos que le corresponden, respetando LSP.



4. Principio de Segregación de Interfaces (ISP) en Rust

Interface Segregation Principle (ISP): los clientes no deben ser forzados a depender de interfaces que no utilizan. En Rust, esto significa diseñar traits pequeños y específicos en lugar de uno grande y general.



Ejemplo de violación de ISP

trait Worker {    fn work(&self);    fn eat(&self);}

struct Human;impl Worker for Human { fn work(&self) { println!("Humano trabajando"); } fn eat(&self) { println!("Humano comiendo"); }}

struct Robot;impl Worker for Robot { fn work(&self) { println!("Robot trabajando"); } fn eat(&self) { panic!("Los robots no comen"); }}

Robot se ve forzado a implementar un método que no necesita.



Implementación correcta de ISP

trait Workable {    fn work(&self);}

trait Eatable { fn eat(&self);}

struct Human;impl Workable for Human { fn work(&self) { println!("Humano trabajando"); }}impl Eatable for Human { fn eat(&self) { println!("Humano comiendo"); }}

struct Robot;impl Workable for Robot { fn work(&self) { println!("Robot trabajando"); }}// Robot no implementa Eatable, lo cual es correcto

Ahora cada tipo implementa solo los traits que necesita.



5. Principio de Inversión de Dependencias (DIP) con traits

Dependency Inversion Principle (DIP): los módulos de alto nivel no deben depender de módulos de bajo nivel; ambos deben depender de abstracciones. Las abstracciones no deben depender de detalles; los detalles deben depender de abstracciones.



Ejemplo de violación de DIP

struct MySQLDatabase;impl MySQLDatabase {    fn save_user(&self, user: &User) {        println!("Guardando usuario en MySQL");    }}

struct UserService { db: MySQLDatabase, // Dependencia concreta}

impl UserService { fn create_user(&self, user: &User) { self.db.save_user(user); }}

UserService depende directamente de MySQLDatabase, lo que dificulta cambiar de base de datos o hacer pruebas.



Implementación correcta de DIP

trait Database {    fn save_user(&self, user: &User);}

struct MySQLDatabase;impl Database for MySQLDatabase { fn save_user(&self, user: &User) { println!("Guardando usuario en MySQL"); }}

struct PostgresDatabase;impl Database for PostgresDatabase { fn save_user(&self, user: &User) { println!("Guardando usuario en PostgreSQL"); }}

struct UserService { db: Box<dyn Database>, // Dependencia abstracta}

impl UserService { fn create_user(&self, user: &User) { self.db.save_user(user); }}

Ahora UserService depende de la abstracción Database, y podemos inyectar cualquier implementación concreta.



Conclusión: SOLID en Rust — código más robusto y mantenible

Hemos visto cómo aplicar los cinco principios SOLID en Rust usando traits, genéricos y composición. Aunque Rust no es un lenguaje orientado a objetos tradicional, sus características permiten implementar estos principios de manera elegante y eficiente. Al aplicar SOLID, tu código Rust será más modular, testeable y fácil de mantener a largo plazo.

Recuerda: los principios son guías, no reglas absolutas. Úsalos con criterio y adapta las soluciones al contexto de tu proyecto. ¡Feliz codificación en Rust!

```

Blogs

Book Recommendations