Inyección de Dependencias en Rust: en la práctica
La Inyección de Dependencias (DI) es un patrón de diseño que permite pasar dependencias a un objeto desde el exterior, en lugar de crearlas internamente. En Rust, este enfoque es especialmente útil gracias al sistema de propiedad, los traits y los genéricos. En este artículo, veremos cómo implementar DI en Rust en la práctica, desde ejemplos simples hasta técnicas avanzadas con contenedores de dependencias.
¿Por qué es importante la DI en Rust?
En Rust, donde no existe la POO clásica con herencia, la DI ayuda a:
- Reducir el acoplamiento — los módulos dependen de abstracciones, no de implementaciones concretas.
- Simplificar las pruebas — puede reemplazar implementaciones reales con mocks.
- Aumentar la flexibilidad — es fácil cambiar el comportamiento del sistema sin modificar su código.
Fundamentos: pasar dependencias a través de parámetros
La forma más sencilla de inyectar una dependencia es pasarla como parámetro de una función o del constructor de una estructura.
// Definimos un trait para la abstraccióntrait Logger { fn log(&self, message: &str);}
// Implementación para consolastruct ConsoleLogger;
impl Logger for ConsoleLogger { fn log(&self, message: &str) { println!("LOG: {}", message); }}
// Implementación para archivostruct FileLogger { filename: String,}
impl Logger for FileLogger { fn log(&self, message: &str) { // En código real, aquí se escribiría en un archivo println!("FILE: {} -> {}", self.filename, message); }}
// Servicio que usa DI a través del constructorstruct UserService<L: Logger> { logger: L,}
impl<L: Logger> UserService<L> { fn new(logger: L) -> Self { Self { logger } }
fn create_user(&self, name: &str) { self.logger.log(&format!("User created: {}", name)); }}
fn main() { let console_logger = ConsoleLogger; let user_service = UserService::new(console_logger); user_service.create_user("Alice");}En este ejemplo, UserService no sabe qué logger se está usando exactamente; solo depende del trait Logger. Esta es la esencia de la DI.
Enfoque avanzado: objetos trait y despacho dinámico
Cuando necesita almacenar diferentes implementaciones en un mismo contenedor, use objetos trait (Box<dyn Trait>).
trait Database { fn query(&self, sql: &str) -> Vec<String>;}
struct PostgresDb;impl Database for PostgresDb { fn query(&self, sql: &str) -> Vec<String> { vec!["result1".to_string(), "result2".to_string()] }}
struct SqliteDb;impl Database for SqliteDb { fn query(&self, sql: &str) -> Vec<String> { vec!["sqlite_result".to_string()] }}
struct App { db: Box<dyn Database>, logger: Box<dyn Logger>,}
impl App { fn new(db: Box<dyn Database>, logger: Box<dyn Logger>) -> Self { Self { db, logger } }
fn run(&self) { let results = self.db.query("SELECT * FROM users"); for r in results { self.logger.log(&r); } }}
fn main() { let app = App::new( Box::new(PostgresDb), Box::new(ConsoleLogger), ); app.run();}La ventaja de los objetos trait es que puede cambiar las implementaciones en tiempo de ejecución. La desventaja es una pequeña sobrecarga por el despacho dinámico.
Contenedor de dependencias: inversión de control
Para proyectos grandes, es conveniente usar un contenedor de dependencias. Aunque Rust no tiene un contenedor DI estándar, es fácil implementarlo manualmente o usar crates como shaku o dice.
Ejemplo de un contenedor simple hecho a mano:
use std::collections::HashMap;use std::any::Any;
// Ti