Injeção de dependência: o padrão que torna código testável e desacoplado
Injeção de dependência é dar a um objeto o que ele precisa em vez de deixá-lo criar; entenda IoC, os tipos de injeção, containers e por que testar fica fácil.
Neste artigo
Toda classe precisa de coisas para funcionar: um banco de dados, um cliente HTTP, um relógio, um gerador de log. A pergunta que separa código fácil de manter de código que resiste a qualquer mudança é simples de enunciar e difícil de internalizar: quem cria essas coisas — a própria classe, por dentro, ou alguém de fora, que entrega prontas? A resposta errada, repetida em milhares de linhas, produz sistemas que funcionam mas não podem ser testados isoladamente e não podem ser mudados sem medo. A resposta certa tem nome: injeção de dependência.
Não é um recurso de linguagem, nem exige framework, nem é exclusividade de código orientado a objetos. É uma decisão de design que se resume a uma frase: um objeto não deveria construir aquilo de que depende; deveria recebê-lo pronto. Este artigo entra a fundo nessa ideia — o problema que ela resolve, sua relação com inversão de controle, os tipos de injeção, o papel (e os limites) dos containers, e os anti-padrões que aparecem quando o padrão é aplicado sem entender por quê ele existe.
O problema: a classe que constrói o que usa#
Considere um serviço que precisa salvar pedidos num banco de dados. A forma mais direta de escrever isso é deixar a própria classe instanciar o banco:
``` class OrderService { constructor() { this.db = new PostgresDatabase("connection-string-de-producao"); }
save(order) { this.db.insert("orders", order); } } ```
Funciona. Compila, roda, salva o pedido. E é exatamente por funcionar que o problema passa despercebido até ficar caro. OrderService está acoplado a PostgresDatabase de um jeito que nenhuma assinatura de método revela: para saber que essa dependência existe, é preciso abrir o construtor e ler o corpo. Três consequências concretas nascem daí.
Testar isoladamente vira impossível. Um teste unitário de OrderService.save() precisa, hoje, de um Postgres real, com uma string de conexão real, rodando em algum lugar. Não existe como substituir o banco por um dublê que apenas verifica se insert foi chamado com os dados certos — o new PostgresDatabase(...) está soldado dentro da classe, e nada de fora alcança para trocá-lo.
Trocar a implementação exige editar o consumidor. Se amanhã o time decide migrar para outro banco, ou precisa rodar contra SQLite em ambiente de desenvolvimento, a mudança acontece dentro de OrderService — e dentro de toda outra classe que também fez new PostgresDatabase(...) por conta própria. A decisão sobre qual banco usar está espalhada pelo código, em vez de concentrada num único lugar.
A dependência fica escondida. Olhando de fora, new OrderService() não revela que a classe fala com um banco de dados, muito menos com qual. Quem usa OrderService só descobre a dependência real lendo a implementação inteira — a assinatura pública mente por omissão.
A mesma classe, com a dependência injetada#
A correção não muda o que a classe faz — muda quem decide com qual banco ela trabalha:
``` class OrderService { constructor(database) { this.db = database; }
save(order) { this.db.insert("orders", order); } }
// em produção const service = new OrderService(new PostgresDatabase(connectionString));
// em teste const service = new OrderService(new FakeDatabase()); ```
OrderService não sabe mais, e não precisa saber, que banco está por trás de database — só sabe que é algo capaz de insert. Isso é injeção de dependência: o ato de fornecer a uma classe aquilo de que ela precisa, de fora, em vez de deixá-la construir sozinha. A classe deixa de ser responsável por duas coisas — sua própria lógica e a montagem de suas dependências — e passa a ser responsável só pela primeira. Quem monta é outra camada, em outro momento.
Repare que nada mudou na lógica de negócio de save(). A mudança inteira está em de onde vem this.db: antes, de dentro; agora, de fora, via parâmetro do construtor. É uma inversão pequena no texto e grande nas consequências.
Inversão de controle: quem decide sobe na cadeia#
Injeção de dependência é a técnica; inversão de controle (IoC) é o princípio mais amplo por trás dela. No modelo tradicional, uma classe controla o fluxo: ela decide quando chamar outra parte do sistema e, no caminho, decide também que implementação concreta usar. Na inversão de controle, esse controle sobe: a classe de baixo nível não decide mais nada sobre suas próprias dependências — apenas declara o que precisa, na forma de uma interface — e uma camada acima decide o que efetivamente será entregue.
O nome informal para essa relação é o princípio de Hollywood: "não nos chame, nós chamamos você". OrderService não vai atrás do banco; o banco (ou melhor, uma implementação escolhida por fora) é entregue a OrderService quando ele é construído. A classe deixou de comandar sua própria composição.
Vale marcar a relação com SOLID, já que o "D" do acrônimo — o princípio de inversão de dependência — é primo direto desta ideia: módulos de alto nível não deveriam depender de módulos de baixo nível, ambos deveriam depender de abstrações. Injeção de dependência é o mecanismo prático que torna esse princípio realizável no código; DIP é a regra de design, DI é como ela se cumpre linha a linha. Este artigo não é sobre SOLID — é sobre o padrão em si, em profundidade — mas a conexão explica por que os dois nomes aparecem sempre juntos.
Por que isso vale o esforço#
O ganho de injetar dependências em vez de construí-las aparece em três frentes, e vale separar cada uma com clareza porque são benefícios de naturezas diferentes.
Testabilidade é o motivo mais imediato. Com a dependência injetada, um teste pode passar um dublê — um mock que grava chamadas, um stub que devolve valores fixos, um fake que simula o comportamento real de forma simplificada, em memória. OrderService testado com FakeDatabase roda em milissegundos, sem rede, sem estado compartilhado entre execuções, sem exigir infraestrutura de teste. Isso não é um detalhe de conveniência: é o que torna viável ter uma suíte de testes rápida o bastante para rodar a cada commit.
Flexibilidade é o ganho estrutural. Trocar PostgresDatabase por SQLiteDatabase em desenvolvimento, ou por CachedDatabase em produção sob carga, não exige tocar em OrderService. A classe que usa a dependência nunca muda quando a implementação muda — só muda o ponto onde a implementação é escolhida. Esse é o mesmo mecanismo que permite adicionar um novo provedor de e-mail, um novo gateway de pagamento, ou um novo cliente de fila sem reescrever quem os consome.
Clareza é o ganho menos falado e igualmente importante. Quando as dependências entram pelo construtor, a assinatura da classe se torna a lista honesta do que ela precisa para existir. new OrderService(database) já avisa, sem precisar ler o corpo, que essa classe depende de um banco. Dependência escondida dentro do corpo do método é dependência que só se descobre lendo — dependência explícita na assinatura é dependência que se lê de relance.
Os três jeitos de injetar#
Existem três formas de entregar uma dependência a um objeto, e elas não são intercambiáveis — cada uma comunica algo diferente sobre a obrigatoriedade da dependência.
Por construtor é a forma preferida. A dependência é passada no momento da criação do objeto e vira um campo, tipicamente imutável:
`` class NotificationService { constructor(emailClient) { this.emailClient = emailClient; // obrigatório, fixado na criação } } ``
O construtor é o lugar certo para dependências obrigatórias: se emailClient não existe, o objeto não deveria existir. Essa forma tem uma propriedade valiosa — torna impossível construir um NotificationService num estado inválido, porque a única forma de criá-lo já exige a dependência. Não existe janela de tempo em que o objeto está "meio pronto".
Por setter ou propriedade serve para dependências opcionais. A dependência é atribuída depois da criação, via um método ou propriedade pública:
`` class ReportGenerator { setLogger(logger) { this.logger = logger; // opcional; se ausente, generator funciona sem log } } ``
Isso comunica que o objeto funciona sem essa dependência — ela apenas enriquece o comportamento quando presente. O custo é que o objeto passa por um estado transitório em que a dependência ainda não foi atribuída, e código que assume que ela sempre está lá pode falhar de forma inesperada se alguém esquecer o setLogger.
Por método ou parâmetro entrega a dependência só na hora do uso. Em vez de guardar a dependência como campo do objeto, ela é passada como argumento da chamada que precisa dela:
`` function processPayment(order, gateway) { return gateway.charge(order.total); } ``
Essa forma é natural quando a dependência muda a cada chamada — processar um pagamento hoje com um gateway, amanhã com outro, sem que isso seja um atributo permanente de quem processa. É também a forma que aparece com mais naturalidade fora do mundo orientado a objetos.
Entre as três, injeção por construtor deveria ser o padrão default sempre que a dependência é essencial ao funcionamento do objeto — é a que produz o código mais difícil de usar errado.
Depender de abstrações, não de implementações#
Injetar uma dependência concreta já resolve o problema de quem a constrói, mas resolve pela metade se o tipo declarado continuar sendo a implementação concreta. O ganho pleno de DI aparece quando o que é injetado é uma abstração — uma interface, um protocolo, um tipo que descreve comportamento sem amarrar a implementação:
``` interface Database { insert(table, record); }
class PostgresDatabase implements Database { / ... / } class FakeDatabase implements Database { / ... / }
class OrderService { constructor(database: Database) { this.db = database; } } ```
Com OrderService dependendo de Database em vez de PostgresDatabase, qualquer implementação da interface serve — real, falsa, em memória, decorada com cache, decorada com log. A classe não sabe, e não deveria saber, qual chegou. Sem essa camada de abstração, trocar a implementação continua exigindo alterar o tipo declarado em cada consumidor; com ela, a troca é só uma linha diferente no lugar que monta o objeto. É essa combinação — dependência injetada e dependência abstrata — que entrega o pacote completo: testabilidade real (o dublê implementa a mesma interface) e flexibilidade real (qualquer implementação nova encaixa sem tocar em quem consome).
O que um container de injeção de dependência resolve#
Em sistemas pequenos, montar os objetos à mão — decidir, num único lugar, "este OrderService recebe este PostgresDatabase, que por sua vez recebe esta configuração de conexão" — é trivial. Em sistemas com dezenas ou centenas de classes, cada uma dependendo de várias outras, montar esse grafo à mão vira trabalho repetitivo e frágil: adicionar uma dependência nova a uma classe no fundo do grafo obriga a editar a cadeia inteira de construções acima dela.
Um container de injeção de dependência (ou framework de IoC) existe para automatizar essa montagem. A mecânica é sempre a mesma em espírito, ainda que a sintaxe varie por linguagem: primeiro se registra o mapeamento entre abstração e implementação —
`` container.register(Database, PostgresDatabase); container.register(EmailClient, SmtpEmailClient); container.register(OrderService); // suas dependências serão resolvidas automaticamente ``
— depois se resolve o objeto de mais alto nível, e o container percorre o grafo de dependências declaradas nos construtores, instanciando cada uma na ordem certa:
`` const service = container.resolve(OrderService); // o container viu que OrderService pede um Database, // olhou o registro, criou um PostgresDatabase, e injetou ``
O valor do container não é injetar dependência — isso o código já fazia manualmente no exemplo anterior deste artigo. O valor é não precisar escrever, à mão, a montagem de um grafo grande e mutável. Trocar uma implementação registrada muda o comportamento do sistema inteiro a partir de uma linha de configuração, sem tocar em nenhum consumidor.
Escopos e as armadilhas que eles escondem#
Todo container de DI de verdade oferece controle sobre o ciclo de vida de cada dependência registrada — quanto tempo uma instância vive antes de ser descartada e recriada. Os três escopos mais comuns aparecem, com nomes variando por ecossistema, mas o conceito é estável:
- Singleton — uma única instância para toda a vida da aplicação, compartilhada por todo mundo que a pede.
- Transient — uma instância nova a cada resolução; ninguém compartilha nada.
- Scoped — uma instância por unidade de trabalho (tipicamente uma requisição HTTP), compartilhada dentro dela e descartada ao final.
Escolher o escopo errado é uma fonte de bugs sutis e caros. Um singleton que guarda estado mutável vaza entre usuários. Se Database for registrado como singleton e, por engano, alguma classe guardar num campo dela o tenant_id da requisição atual, a segunda requisição concorrente encontra o tenant_id da primeira ainda ali — um vazamento de dado entre contextos que não aparece em teste sequencial e aparece só sob carga real. A regra prática é: singleton só para o que é genuinamente sem estado ou imutável por natureza (um cliente HTTP configurado, um logger).
Dependência capturada é a armadilha espelhada. Acontece quando um objeto de vida longa (singleton) injeta, na hora de sua própria construção, um objeto de vida curta (scoped ou transient) — e passa a segurar essa instância curta para sempre, porque a captura acontece uma vez só, no momento em que o singleton nasce. O resultado é uma dependência scoped que deveria renovar a cada requisição, mas fica congelada na primeira instância criada, entregando dado desatualizado ou de outro contexto para sempre. A maioria dos containers modernos detecta e recusa esse tipo de registro incoerente, mas o risco existe em qualquer configuração manual.
Quando o container é exagero#
DI é um padrão de design; container é uma ferramenta. Confundir os dois leva a instalar um framework de IoC completo, com configuração própria e curva de aprendizado, num projeto pequeno o bastante para não precisar — e a inversão de controle, nesse caso, some atrás da complexidade de configurar o próprio container.
Para grafos de objetos pequenos ou médios, a alternativa é DI manual, também chamada de pure DI ou poor man's DI: escrever, à mão, o código que instancia cada objeto com suas dependências, num único ponto do programa. Esse ponto tem nome — o composition root: o único lugar, na borda da aplicação (tipicamente perto do ponto de entrada), que tem permissão de conhecer as implementações concretas e montá-las.
`` // composition root — o único lugar que conhece PostgresDatabase, SmtpEmailClient etc. function bootstrap() { const db = new PostgresDatabase(config.connectionString); const emailClient = new SmtpEmailClient(config.smtpHost); const orderService = new OrderService(db); const notificationService = new NotificationService(emailClient); return { orderService, notificationService }; } ``
Fora do composition root, nenhuma outra parte do código deveria escrever new PostgresDatabase(...) — é ali, e só ali, que a decisão sobre implementação concreta é tomada. Pure DI é mais verboso à medida que o grafo cresce, mas é também mais explícito: ler o composition root mostra o sistema inteiro montado, sem indireção de configuração de container para decifrar. A escolha entre container e DI manual é de escala e de time, não de correção — nenhuma das duas é "a forma certa" universal.
Anti-padrões que negam o próprio benefício#
Alguns usos de DI reintroduzem, por um caminho lateral, exatamente o acoplamento que o padrão existe para remover.
Service locator esconde a dependência em vez de declará-la. Em vez de receber database como parâmetro do construtor, a classe chama um localizador global — ServiceLocator.get(Database) — de dentro do próprio corpo. A dependência volta a estar invisível na assinatura, exatamente como no exemplo acoplado do início deste artigo, só que agora escondida atrás de uma chamada em vez de um new. O teste continua difícil, porque trocar o que o locator devolve exige mexer em estado global compartilhado entre testes.
Injetar o próprio container é o mesmo erro com outro nome. Uma classe que recebe o container inteiro no construtor, e chama container.resolve(X) sempre que precisa de algo, não declarou nenhuma dependência real — declarou uma dependência de tudo, indiretamente. A assinatura deixa de comunicar qualquer coisa útil, e o objeto pode, em teoria, pedir qualquer serviço do sistema a qualquer momento. Isso é service locator com um nome mais respeitável.
Um construtor com muitos parâmetros é um sintoma, não um problema de DI. Quando uma classe acumula seis, oito, dez dependências injetadas, o padrão está funcionando corretamente — está deixando visível algo que estava escondido antes: essa classe faz coisas demais. A resposta certa não é esconder as dependências atrás de um objeto de configuração ou de um service locator para "limpar" a assinatura; é dividir a classe. DI não causa esse problema, ele o revela — e revelar problemas de design é, em si, um dos benefícios do padrão.
DI sem orientação a objetos#
Nada do que foi descrito exige classes, interfaces ou um vocabulário orientado a objetos. Em estilo funcional, a mesma ideia aparece como passar funções ou valores como parâmetro em vez de referenciar algo fixo de dentro da função:
``` // acoplado: a função de validação está fixada por dentro function processOrder(order) { if (!defaultValidator(order)) throw new Error("inválido"); }
// com a dependência injetada como parâmetro function processOrder(order, validate) { if (!validate(order)) throw new Error("inválido"); }
processOrder(order, strictValidator); // produção processOrder(order, () => true); // teste, sem validação real ```
validate é uma dependência injetada tanto quanto database era no primeiro exemplo — só que a abstração, aqui, é o tipo da função em vez de uma interface nomeada, e a injeção acontece por parâmetro em vez de por construtor. O princípio é o mesmo: quem usa não decide o que usar; quem chama decide, e entrega pronto. Isso mostra que DI não é sintaxe de framework — é uma forma de pensar sobre onde uma decisão deveria morar, aplicável em qualquer paradigma que tenha alguma noção de passar algo como argumento.
Fechando#
Injeção de dependência começa numa pergunta pequena — quem cria o que um objeto precisa, ele mesmo ou alguém de fora — e a resposta certa se propaga em benefícios que só aparecem depois, quando é preciso testar sem infraestrutura real, trocar uma implementação sem caçar todos os lugares que a referenciam, ou simplesmente ler uma assinatura e saber do que aquele código depende. Prefira injeção por construtor para o que é obrigatório, dependa de abstrações e não de implementações concretas, e reserve o container para quando o grafo de objetos realmente justificar a ferramenta — em grafos pequenos, montar tudo à mão no composition root é mais explícito, não menos correto. O valor do padrão nunca esteve no framework que o automatiza; esteve sempre no desacoplamento e na testabilidade que ele obriga a existir, mesmo escrito à mão, mesmo sem uma única anotação de container no código.