Existe uma sequência bastante comum em projetos Python: você escreve uma classe, ela funciona bem, aí chega a hora de testar — e percebe que não dá para testar sem subir um banco de dados, sem fazer uma chamada HTTP real, sem criar um arquivo em disco. O código funciona, mas ele não é testável. E não testável, na prática, significa frágil.
O problema quase sempre tem a mesma raiz: a classe criou as próprias dependências em vez de recebê-las.
Este artigo trata de Injeção de Dependência — o que é, por que resolve esse problema, e como implementar manualmente em Python antes de cogitar qualquer biblioteca. Entender o mecanismo primeiro é o que distingue usar DI de simplesmente seguir um tutorial.
1. O Problema: Dependências Acopladas
Veja este código. Ele é direto, funciona, e provavelmente se parece com código que você já escreveu ou leu:
| |
O problema não é o que o código faz. É o que ele impossibilita:
- Não dá para testar
notificarsem um servidor SMTP acessível - Não dá para trocar o canal de notificação (push, Slack, SMS) sem alterar a classe
- Não dá para usar um cliente SMTP diferente em produção e em staging sem condicional no código
- Toda instância de
NotificadorDePedidotraz umsmtplib.SMTPembutido — sem negociação
NotificadorDePedido sabe demais. Ela sabe que a notificação é por e-mail, que o servidor é smtp.empresa.com, e que a porta é 587. Conhecimento que não é responsabilidade dela.
2. O que é Injeção de Dependência
Injeção de Dependência (DI) é um nome sofisticado para uma ideia simples: em vez de uma classe criar suas dependências, ela as recebe.
Quem decide qual dependência usar é quem instancia a classe — não a classe em si. Isso inverte o controle: a classe para de controlar a criação e passa a declarar o que precisa.
Vale situar o termo numa hierarquia que aparece bastante em discussões sobre o tema:
- IoC (Inversão de Controle) é o princípio mais amplo: quem controla o fluxo e a criação de objetos não é mais o próprio objeto, mas algo externo a ele.
- DI (Injeção de Dependência) é um padrão específico de IoC: a dependência é fornecida externamente — por construtor, método ou propriedade.
- Container de DI é uma ferramenta: automatiza a composição quando o grafo de dependências cresce a ponto de gerenciar manualmente se tornar custoso.
Esta é a definição que usamos aqui, a mesma estabelecida por Martin Fowler no artigo Inversion of Control Containers and the Dependency Injection pattern (2004) — que cunhou o termo e classifica constructor injection, setter injection e interface injection como formas legítimas de DI, sem exigir container. O container é opcional; o padrão, não.
Antes de qualquer abstração ou framework, isso se resume a passar a dependência pelo construtor:
| |
É só isso. Não tem mágica. A classe continua fazendo exatamente o mesmo trabalho — mas agora quem chama decide o que passa. Em produção, passa um smtplib.SMTP real. Em teste, passa um objeto falso que nem abre conexão.
3. As Três Formas de Injetar
Dependências podem ser injetadas de três formas diferentes. Cada uma tem seu lugar.
3.1 Injeção por Construtor
A mais comum e, na maioria dos casos, a mais correta. A dependência é declarada no __init__ e fica disponível para toda a vida do objeto:
| |
Use quando a dependência é essencial para o funcionamento do objeto — sem ela, o objeto não faz sentido existir.
3.2 Injeção por Método
A dependência é passada diretamente na chamada do método. Útil quando a dependência muda a cada chamada ou quando nem sempre é necessária:
| |
Use quando a dependência é contextual — a mesma instância da classe pode usar formatadores diferentes em chamadas diferentes.
3.3 Injeção por Propriedade
A dependência é atribuída depois da criação do objeto. Menos comum, útil para dependências opcionais:
| |
Use com moderação. Dependências opcionais tornam o comportamento do objeto menos previsível — quem lê o código precisa saber o que muda com e sem o logger.
4. Abstraindo com Protocols
O exemplo anterior já é uma melhoria real, mas ainda tem uma fragilidade: NotificadorDePedido aceita qualquer coisa como smtp_client. Se alguém passar um objeto que não tem send_message, o erro só aparece em runtime, na hora da chamada.
Em Python moderno, o lugar certo para declarar o contrato da dependência é um Protocol:
| |
Protocol define o contrato sem forçar herança. Qualquer objeto que implemente send_message(mensagem: MIMEText) -> None satisfaz ClienteEmail — o smtplib.SMTP real, um mock de teste, um cliente de terceiros. Nenhum deles precisa herdar de ClienteEmail explicitamente. O mypy verifica isso em tempo de análise estática, não em runtime.
Essa é a diferença em relação a ABC (Abstract Base Class): com ABC, as classes precisam herdar e declarar conformidade. Com Protocol, a conformidade é estrutural — se tem os métodos certos, satisfaz o contrato. Em Python, isso costuma ser a escolha mais flexível para definir dependências.
5. Testabilidade na Prática
Aqui é onde a diferença fica concreta. Com a dependência injetável, escrever um teste unitário deixa de exigir infraestrutura real:
| |
O teste não abre conexão, não precisa de variável de ambiente, não falha por indisponibilidade de rede. Ele testa exatamente o que deveria testar: o comportamento de NotificadorDePedido.
ClienteEmailFalso satisfaz o Protocol sem herdar nada — basta implementar send_message com a assinatura correta. O mypy confirma isso em análise estática.
6. Um Exemplo Mais Completo — Camadas Reais
DI começa a mostrar seu valor de verdade quando há múltiplas camadas. Veja um caso típico: um serviço de pedidos que depende de repositório e notificador.
Definindo os contratos:
| |
A camada de serviço — sem saber nada de banco ou e-mail:
| |
ServicoDePedidos não importa SQLAlchemy, não importa smtplib. Ela conhece apenas os contratos — RepositorioDePedidos e Notificador. Trocar PostgreSQL por SQLite, ou e-mail por Slack, não toca nesta classe.
As implementações reais ficam em outro lugar:
| |
E os testes ficam limpos, sem infraestrutura:
| |
Toda a lógica de negócio de confirmar_pedido está coberta sem tocar em banco de dados ou rede. Cada teste é determinístico, rápido e isolado.
7. Composição na Borda da Aplicação
Com DI manual, alguém precisa montar as dependências. Esse ponto de montagem tem um nome: composition root — a borda da aplicação, onde tudo se conecta.
Em um projeto Python típico, esse lugar é o main.py, o arquivo de startup, ou a factory da aplicação:
| |
O que o composition root deixa claro: ServicoDePedidos não sabe que existe SQLite. NotificadorDePedido não sabe que existe ServicoDePedidos. Cada peça conhece apenas o contrato da peça ao lado.
8. Quando Considerar uma Biblioteca
DI manual funciona bem — e para a maioria dos projetos, é tudo o que você precisa. Mas há situações onde a complexidade da composição começa a crescer: muitas dependências, ciclos de vida diferentes (singleton vs. transient), recriação de dependências por request em APIs.
Quando esse ponto chegar, as principais opções no ecossistema Python são dependency-injector e lagom. Ambas adicionam um container que gerencia a criação e o ciclo de vida das dependências — mas o que elas fazem é, no fundo, automatizar exatamente o que o criar_servico() faz acima.
Quem entende o mecanismo manual usa essas bibliotecas com clareza. Quem pula direto para o framework tende a tratar o container como uma caixa preta — e quando algo dá errado na composição, não sabe onde olhar.
9. Checklist de Boas Práticas
| # | Prática | Por quê |
|---|---|---|
| 1 | Injete dependências pelo construtor quando são essenciais | Torna obrigatórias as dependências que o objeto não pode funcionar sem |
| 2 | Use Protocol para definir contratos, não ABC | Conformidade estrutural: nenhuma herança forçada nas implementações |
| 3 | Crie implementações falsas simples nos testes | Mais controle e clareza do que MagicMock para dependências com contrato definido |
| 4 | Concentre a composição em um único ponto | Facilita entender o grafo de dependências e trocar implementações |
| 5 | Mantenha a camada de serviço sem imports de infraestrutura | Se import sqlite3 aparece no serviço, algo está errado |
| 6 | Não injete mais do que o necessário | Uma classe que recebe 5 dependências provavelmente tem responsabilidades demais |
| 7 | Documente os contratos com docstrings nos Protocols | O Protocol é a interface pública — merece a mesma atenção que o código |
10. Conclusão
Injeção de Dependência não é uma feature do framework. É uma decisão de design — e uma das mais impactantes que você pode tomar num projeto Python.
O que muda na prática: classes que declaram o que precisam em vez de criar o que precisam. Testes que verificam comportamento sem depender de infraestrutura. Código que pode ser lido, alterado e estendido sem surpresas.
Nenhuma dessas propriedades exige biblioteca. Exige disciplina na hora de projetar a interface das classes — e é exatamente aí que Protocol se encaixa: define o contrato, deixa o mypy verificar, e permite que qualquer implementação entre sem herança forçada.
Há uma conexão direta com o que já discutimos nos artigos sobre arquitetura hexagonal e primitive obsession: a camada de domínio não deve conhecer infraestrutura. DI é o mecanismo que torna isso possível na prática — sem ela, a arquitetura hexagonal fica no papel.
Se este artigo te fez repensar como você conecta as peças de um projeto, compartilhe: @riverfount@bolha.us