Introducción: ¿Por qué es importante Dependency Injection en Rust?
\n\nAl escribir programas en Rust, tarde o temprano te encontrarás con una situación en la que un módulo (por ejemplo, un servicio o controlador) necesita usar las capacidades de otro módulo (base de datos, API, registrador). La forma más simple es crear el objeto necesario directamente dentro del código que lo utiliza:
\n\nstruct UserService;\n\nimpl UserService {\n fn get_user(&self, id: u32) -> String {\n let db = Database::new(); // ❌ Acoplamiento rígido\n db.query_user(id)\n }\n}\n\n\nEste enfoque crea un acoplamiento rígido (tight coupling). El código se vuelve difícil de probar, reutilizar y modificar. Dependency Injection (DI) es un patrón en el que las dependencias se pasan al objeto desde el exterior, en lugar de crearse dentro de él. Rust, al ser un lenguaje con un sistema de propiedad y traits, ofrece formas elegantes de implementar DI sin frameworks.
\n\nEn este artículo, analizaremos cómo inyectar dependencias en Rust con ejemplos prácticos: desde la simple transferencia de estructuras hasta el uso de traits y punteros inteligentes.
\n\n1. Enfoque básico: Pasar dependencias a través del constructor
\n\nLa forma más sencilla de inyectar una dependencia en Rust es pasarla como argumento al constructor o a la función. Consideremos un ejemplo con un registrador.
\n\nEjemplo incorrecto (sin DI)
\n\nstruct ConsoleLogger;\n\nimpl ConsoleLogger {\n fn log(&self, msg: &str) {\n println!("[LOG]: {}", msg);\n }\n}\n\nstruct OrderService {\n logger: ConsoleLogger, // ❌ Vinculación rígida a un tipo específico\n}\n\nimpl OrderService {\n fn new() -> Self {\n Self {\n logger: ConsoleLogger, // Se crea internamente\n }\n }\n\n fn place_order(&self, item: &str) {\n self.logger.log(&format!("Pedido: {}", item));\n }\n}\n\n\nEjemplo correcto (con DI)
\n\nstruct ConsoleLogger;\n\nimpl ConsoleLogger {\n fn log(&self, msg: &str) {\n println!("[LOG]: {}", msg);\n }\n}\n\nstruct OrderService {\n logger: ConsoleLogger, // La dependencia se pasa desde el exterior\n}\n\nimpl OrderService {\n // El constructor acepta la dependencia\n fn new(logger: ConsoleLogger) -> Self {\n Self { logger }\n }\n\n fn place_order(&self, item: &str) {\n self.logger.log(&format!("Pedido: {}", item));\n }\n}\n\nfn main() {\n let logger = ConsoleLogger;\n let service = OrderService::new(logger);\n service.place_order("Libro sobre Rust");\n}\n\n\n¿Qué cambió? Ahora OrderService no crea el registrador por sí mismo. Lo recibe ya listo. Esto ya es DI, pero hay un matiz: todavía estamos vinculados al tipo específico ConsoleLogger. Para hacer el código flexible, necesitamos usar traits.
2. Uso de traits para la abstracción de dependencias
\n\nLos traits en Rust son el equivalente a las interfaces en otros lenguajes. Permiten definir un contrato que las dependencias deben cumplir.
\n\nDefinimos el trait Logger
\n\ntrait Logger {\n fn log(&self, msg: &str);\n}\n\nstruct ConsoleLogger;\nimpl Logger for ConsoleLogger {\n fn log(&self, msg: &str) {\n println!("[Consola]: {}", msg);\n }\n}\n\nstruct FileLogger {\n filename: String,\n}\nimpl Logger for FileLogger {\n fn log(&self, msg: &str) {\n // Simulación de escritura en archivo\n println!("[Archivo {}]: {}", self.filename, msg);\n }\n}\n\n\nInyectamos el trait mediante genéricos (generics)
\n\nstruct OrderService<L: Logger> {\n logger: L, // Tipo genérico que implementa el trait Logger\n}\n\nimpl<L: Logger> OrderService<L> {\n fn new(logger: L) -> Self {\n Self { logger }\n }\n\n fn place_order(&self, item: &str) {\n self.logger.log(&format!("Pedido: {}", item));\n }\n}\n\nfn main() {\n let console_logge