Introdução: Por que Dependency Injection é importante em Rust?
\n\nAo escrever programas em Rust, mais cedo ou mais tarde você enfrentará uma situação em que um módulo (por exemplo, um serviço ou controlador) precisa usar as capacidades de outro módulo (banco de dados, API, logger). A maneira mais simples é criar o objeto necessário diretamente dentro do código que o utiliza:
\n\nstruct UserService;\n\nimpl UserService {\n fn get_user(&self, id: u32) -> String {\n let db = Database::new(); // ❌ Acoplamento rígido\n db.query_user(id)\n }\n}\n\n\nEssa abordagem cria um acoplamento rígido (tight coupling). O código se torna difícil de testar, reutilizar e modificar. Dependency Injection (DI) é um padrão onde as dependências são passadas para o objeto de fora, em vez de serem criadas dentro dele. Rust, sendo uma linguagem com sistema de propriedade e traits, oferece maneiras elegantes de implementar DI sem frameworks.
\n\nNeste artigo, vamos explorar como injetar dependências em Rust com exemplos práticos: desde a simples passagem de structs até o uso de traits e ponteiros inteligentes.
\n\n1. Abordagem básica: Passando dependências através do construtor
\n\nA maneira mais simples de injetar uma dependência em Rust é passá-la como argumento do construtor ou função. Vamos considerar um exemplo com um logger.
\n\nExemplo ruim (sem 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, // ❌ Acoplamento rígido a um tipo específico\n}\n\nimpl OrderService {\n fn new() -> Self {\n Self {\n logger: ConsoleLogger, // Criado internamente\n }\n }\n\n fn place_order(&self, item: &str) {\n self.logger.log(&format!("Pedido: {}", item));\n }\n}\n\n\nExemplo bom (com 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, // Dependência passada de fora\n}\n\nimpl OrderService {\n // Construtor recebe a dependência\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("Livro sobre Rust");\n}\n\n\nO que mudou? Agora OrderService não cria o logger por conta própria. Ele o recebe pronto. Isso já é DI, mas há uma nuance: ainda estamos presos ao tipo específico ConsoleLogger. Para tornar o código flexível, precisamos usar traits.
2. Usando traits para abstrair dependências
\n\nTraits em Rust são análogos às interfaces em outras linguagens. Eles permitem definir um contrato que as dependências devem seguir.
\n\nDefinindo o 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!("[Console]: {}", msg);\n }\n}\n\nstruct FileLogger {\n filename: String,\n}\nimpl Logger for FileLogger {\n fn log(&self, msg: &str) {\n // Simulação de escrita em arquivo\n println!("[Arquivo {}]: {}", self.filename, msg);\n }\n}\n\n\nInjetando o trait através de genéricos (generics)
\n\nstruct OrderService<L: Logger> {\n logger: L, // Tipo genérico que implementa o 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