Introducción a la Inyección de Dependencias en Rust
La Inyección de Dependencias (DI) es un patrón de diseño donde un objeto recibe sus dependencias de fuentes externas en lugar de crearlas por sí mismo. En el ecosistema de Rust, este enfoque adquiere especial relevancia debido a su estricto sistema de tipos y modelo de propiedad. DI hace que el código sea más modular, comprobable y mantenible, lo cual es especialmente importante para proyectos grandes.
A diferencia de lenguajes con tipado dinámico o recolección de basura, Rust requiere un enfoque especial para la inyección de dependencias. No existe reflexión ni contenedores en tiempo de ejecución en el sentido clásico. En su lugar, los desarrolladores utilizan mecanismos estáticos: objetos trait, genéricos y composición mediante structs.
En este artículo, exploraremos tres enfoques principales para implementar la Inyección de Dependencias en Rust, comenzando desde la inyección manual más simple hasta el uso de librerías. Cada enfoque se ilustrará con ejemplos prácticos de código.
1. Inyección Manual de Dependencias (Manual DI)
La forma más simple y transparente es pasar las dependencias a través del constructor del struct. Este enfoque no requiere librerías adicionales y está completamente controlado por el compilador de Rust.
Ejemplo: Logger y Servicio
// Define un trait para loggingtrait Logger { fn log(&self, message: &str);}
// Implementación de Logger para producciónstruct ConsoleLogger;
impl Logger for ConsoleLogger { fn log(&self, message: &str) { println!("[LOG]: {}", message); }}
// Implementación para pruebasstruct MockLogger { pub messages: Vec<String>,}
impl Logger for MockLogger { fn log(&self, message: &str) { self.messages.push(message.to_string()); }}
// Servicio que depende de Loggerstruct UserService<T: Logger> { logger: T,}
impl<T: Logger> UserService<T> { fn new(logger: T) -> Self { Self { logger } }
fn create_user(&self, name: &str) { self.logger.log(&format!("Usuario creado: {}", name)); }}
fn main() { let logger = ConsoleLogger; let service = UserService::new(logger); service.create_user("Alexey");}Ventajas:
- Seguridad de tipos completa en tiempo de compilación
- Máximo rendimiento (abstracción de costo cero)
- Facilidad de comprensión y depuración
Desventajas:
- El código se vuelve engorroso a medida que crece el número de dependencias
- Difícil cambiar implementaciones en tiempo de ejecución
- Requiere pasar explícitamente todas las dependencias a través de la cadena de llamadas
2. Inyección mediante objetos trait (Dynamic Dispatch)
Si necesitas la capacidad de cambiar implementaciones en tiempo de ejecución u ocultar el tipo concreto, utiliza objetos trait (Box<dyn Trait>). Este es un enfoque clásico para DI, conocido de otros lenguajes.
Ejemplo: Repositorio Abstracto
use std::sync::Mutex;
trait UserRepository { fn find_by_id(&self, id: u32) -> Option<String>; fn save(&self, user: String);}
struct InMemoryRepository { users: Mutex<Vec<String>>,}
impl UserRepository for InMemoryRepository { fn find_by_id(&self, id: u32) -> Option<String> { let users = self.users.lock().unwrap(); users.get(id as usize).cloned() }
fn save(&self, user: String) { let mut users = self.users.lock().unwrap(); users.push(user); }}
struct UserController { repository: Box<dyn UserRepository>,}
impl UserController { fn new(repository: Box<dyn UserRepository>) -> Self { Self { repository } }
fn get_user(&self, id: u32) -> Option<String>