Injeção de Dependência em Rust: na prática
Injeção de Dependência (DI) é um padrão de projeto que permite passar dependências para um objeto externamente, em vez de criá-las internamente. Em Rust, essa abordagem é especialmente útil graças ao sistema de ownership, traits e genéricos. Neste artigo, veremos como implementar DI em Rust na prática, desde exemplos simples até técnicas avançadas com contêineres de dependências.
Por que DI é importante em Rust?
Em Rust, onde não há POO clássica com herança, DI ajuda a:
- Reduzir o acoplamento — módulos dependem de abstrações, não de implementações concretas.
- Simplificar testes — você pode substituir implementações reais por mocks.
- Aumentar a flexibilidade — é fácil mudar o comportamento do sistema sem alterar seu código.
Fundamentos: passando dependências por parâmetros
A maneira mais simples de injetar dependência é passá-la como parâmetro de uma função ou construtor de struct.
// Definindo um trait para abstraçãotrait Logger { fn log(&self, message: &str);}
// Implementação para consolestruct ConsoleLogger;
impl Logger for ConsoleLogger { fn log(&self, message: &str) { println!("LOG: {}", message); }}
// Implementação para arquivostruct FileLogger { filename: String,}
impl Logger for FileLogger { fn log(&self, message: &str) { // Em código real, aqui haveria escrita em arquivo println!("FILE: {} -> {}", self.filename, message); }}
// Serviço usando DI via construtorstruct 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");}Neste exemplo, UserService não sabe qual logger está sendo usado — ele depende apenas do trait Logger. Essa é a essência da DI.
Abordagem avançada: trait objects e despacho dinâmico
Quando você precisa armazenar diferentes implementações em um único contêiner, use trait objects (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();}A vantagem dos trait objects é que você pode trocar implementações em tempo de execução. A desvantagem é uma pequena sobrecarga no despacho dinâmico.
Contêiner de dependências: inversão de controle
Para projetos grandes, é conveniente usar um contêiner de dependências. Embora Rust não tenha um contêiner DI padrão, é fácil implementar um manualmente ou usar crates como shaku ou dice.
Exemplo de um contêiner simples manual:
use std::collections::HashMap;use std::any::Any;
// Ti