Injeção de Dependência em Rust: Guia Completo para Desenvolvedores

Treinador Online de Python para Iniciantes

Aprenda Python facilmente sem sobrecarga teórica. Resolva tarefas práticas com verificação automática e escreva código diretamente no navegador.

Start Course

Introdução à Injeção de Dependência em Rust



Injeção de Dependência (DI) é um padrão de projeto onde um objeto recebe suas dependências externamente, em vez de criá-las por conta própria. No ecossistema Rust, essa abordagem ganha importância especial devido ao sistema estrito de tipos e propriedade. DI permite tornar o código mais modular, testável e sustentável, o que é especialmente importante para projetos grandes.



Diferente de linguagens com tipagem dinâmica ou coleta de lixo, Rust exige uma abordagem específica para injeção de dependências. Aqui não há reflexão ou contêineres em tempo de execução no sentido clássico. Em vez disso, os desenvolvedores usam mecanismos estáticos: trait objects, generics e composição através de structs.



Neste artigo, exploraremos três abordagens principais para implementar Injeção de Dependência em Rust, começando pela injeção manual mais simples até o uso de bibliotecas. Cada abordagem será ilustrada com exemplos práticos de código.



1. Injeção Manual de Dependências (Manual DI)



A maneira mais simples e transparente é passar dependências através do construtor da struct. Essa abordagem não requer bibliotecas adicionais e é totalmente controlada pelo compilador Rust.



Exemplo: Logger e Serviço



// Definindo um trait para loggingtrait Logger {    fn log(&self, message: &str);}

// Implementação do logger para produçãostruct ConsoleLogger;

impl Logger for ConsoleLogger { fn log(&self, message: &str) { println!("[LOG]: {}", message); }}

// Implementação para testesstruct MockLogger { pub messages: Vec<String>,}

impl Logger for MockLogger { fn log(&self, message: &str) { self.messages.push(message.to_string()); }}

// Serviço dependente 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!("Usuário criado: {}", name)); }}

fn main() { let logger = ConsoleLogger; let service = UserService::new(logger); service.create_user("Alexei");}


Vantagens:

  • Segurança total de tipos em tempo de compilação
  • Máximo desempenho (zero-cost abstraction)
  • Simplicidade de compreensão e depuração


Desvantagens:

  • Com o aumento do número de dependências, o código se torna volumoso
  • Difícil alterar implementações em tempo de execução
  • Exige passar explicitamente todas as dependências através da cadeia de chamadas


2. Injeção via trait objects (Dynamic Dispatch)



Se você precisa da capacidade de alterar implementações em tempo de execução ou ocultar o tipo específico, use trait objects (Box<dyn Trait>). Esta é a abordagem clássica para DI, conhecida de outras linguagens.



Exemplo: Repositório abstrato



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>

Blogs

Book Recommendations