Dependency Injection | Rust
- Dependency Injection | Rust
Dependency Injection (DI) no Rust
O que é DI
Dependency Injection (DI) é um padrão onde uma função, struct ou módulo recebe suas dependências prontas, em vez de criá-las internamente.
Em outras palavras:
- ✅ Quem usa uma dependência não deve criar
- ✅ Quem cria, deve injetar para quem precisa usar
Por que DI é útil no Rust
DI ajuda a manter o código:
- mais organizado
- mais testável
- com menos acoplamento
- mais fácil de trocar implementações (ex: Postgres → Mock)
No Rust isso é especialmente importante porque você geralmente quer:
- inicializar recursos pesados apenas uma vez (pool do banco, clients HTTP, configs)
- reaproveitar esses recursos com segurança (clonando handles como
PgPool)
Problema: criar dependências dentro do handler
Exemplo não recomendado (acoplado e ineficiente):
async fn health() -> String {
let pool = connect_db().await; // cria toda vez
"ok".to_string()
}
Principais problemas:
- cria conexão/pool a cada request
- piora performance
- dificulta testar
- mistura responsabilidade de infraestrutura com regra de negócio
DI por parâmetro de função
Esse é o tipo mais simples de DI.
fn calcular_total(taxa: f64) -> f64 {
100.0 * taxa
}
Aqui taxa é uma dependência “injetada” via argumento.
DI por struct (Services e Repositories)
Esse é o padrão mais comum em aplicações Rust “bem separadas por camadas”.
Exemplo:
use sqlx::PgPool;
pub struct UserService {
db: PgPool,
}
impl UserService {
pub fn new(db: PgPool) -> Self {
Self { db }
}
pub async fn ping(&self) -> Result<(), sqlx::Error> {
sqlx::query("SELECT 1")
.execute(&self.db)
.await?;
Ok(())
}
}
Características:
UserServicedepende do banco (PgPool)- o banco é recebido no
new(...) - o service não sabe “como” o banco foi criado
DI em Actix-web com web::Data
No Actix, DI é feito via estado compartilhado de aplicação com web::Data<T>.
Você registra a dependência no App:
use actix_web::{web, App};
use sqlx::PgPool;
App::new()
.app_data(web::Data::new(pool.clone()))
E recebe automaticamente no handler:
use actix_web::{web, Responder};
use sqlx::PgPool;
async fn health(db: web::Data<PgPool>) -> impl Responder {
"ok"
}
Esse db: web::Data<PgPool> é DI porque:
- o handler não cria
PgPool - o Actix fornece a instância configurada
- a dependência fica centralizada no
main
Como o Actix injeta o PgPool
O fluxo padrão é:
- Você cria o
PgPooluma vez nomain - Registra o pool como
App Statecomweb::Data - O Actix detecta
web::Data<PgPool>na assinatura do handler - Ele injeta automaticamente o valor correto em cada requisição
Exemplo:
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let pool = connect_db().await;
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(pool.clone()))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
DI em camadas (Handler → Service)
Separar camadas melhora manutenção e testes.
Exemplo de handler usando service:
use actix_web::{web, HttpResponse, Responder};
use sqlx::PgPool;
pub async fn health(db: web::Data<PgPool>) -> impl Responder {
let service = HealthService::new(db.get_ref().clone());
match service.check().await {
true => HttpResponse::Ok().body("ok"),
false => HttpResponse::InternalServerError().body("fail"),
}
}
Service:
use sqlx::PgPool;
pub struct HealthService {
db: PgPool,
}
impl HealthService {
pub fn new(db: PgPool) -> Self {
Self { db }
}
pub async fn check(&self) -> bool {
sqlx::query("SELECT 1")
.execute(&self.db)
.await
.is_ok()
}
}
DI com trait para facilitar testes
Quando você quer desacoplar de uma implementação específica, você pode usar trait.
Definindo uma interface:
#[async_trait::async_trait]
pub trait UserRepository {
async fn count_users(&self) -> i64;
}
Implementação real (Postgres):
use sqlx::PgPool;
pub struct PgUserRepository {
db: PgPool,
}
#[async_trait::async_trait]
impl UserRepository for PgUserRepository {
async fn count_users(&self) -> i64 {
10
}
}
Implementação fake (teste):
pub struct FakeUserRepository;
#[async_trait::async_trait]
impl UserRepository for FakeUserRepository {
async fn count_users(&self) -> i64 {
42
}
}
Nesse modelo, o service recebe dyn UserRepository por DI.
Padrões comuns de DI no Rust
As formas mais usadas são:
- passar dependências por parâmetro de função
- usar
struct + new(...)para injetar dependências - usar
web::Data<T>(em Actix) como container simples de estado - usar
traitpara permitir troca de implementação (mock, fake, postgres)
Boas práticas de DI para Postgres (SQLx)
Recomendado:
- criar
PgPoolapenas uma vez nomain - compartilhar o pool via
web::Data<PgPool> - passar
PgPoolpara services/repositories vianew(...) - usar
clone()do pool sem medo (é barato e seguro) - manter queries fora de handler quando possível
Erros comuns
Evite:
- criar conexão/pool dentro de cada handler
- misturar lógica HTTP com queries diretamente no handler
- espalhar
.env/env::var()em vários lugares do código - passar
String DATABASE_URLao invés do pool em todos os lugares