Injeção de Dependência em Rust: na prática

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

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

Blogs

Book Recommendations