Pular para o conteúdo
Categoria: Fundamentos & Boas Práticas13 min de leitura

O que é injeção de dependência? Inversão de controle explicada

Por Lucas Andrade ·

Aprenda a inverter o controle das dependências do seu código para torná-lo mais desacoplado, testável e flexível, com exemplos práticos de DI, IoC e composition root.

O que é injeção de dependência? Inversão de controle explicada

Quando uma classe cria, por conta própria, tudo de que precisa para funcionar, ela fica amarrada a essas escolhas concretas — e mudar qualquer uma delas vira um problema. A injeção de dependência é uma técnica simples que resolve isso: em vez de uma classe construir suas dependências, ela as recebe de fora. Neste artigo você vai entender injeção de dependência, inversão de controle, os tipos de injeção, o papel do composition root e como esses conceitos deixam seu código mais flexível e testável.

O problema: dependências acopladas

Considere uma classe que envia notificações por e-mail:

class ServicoDeNotificacao {
  private email = new ClienteSMTP('smtp.exemplo.com', 587)

  notificar(usuario: Usuario, mensagem: string) {
    this.email.enviar(usuario.email, mensagem)
  }
}

Parece inofensivo, mas há um acoplamento rígido escondido. ServicoDeNotificacao cria seu próprio ClienteSMTP e conhece detalhes da configuração (host, porta). Consequências:

  • Impossível trocar de canal. Para enviar por SMS em vez de e-mail, é preciso alterar a classe.
  • Difícil de testar. Qualquer teste tentará abrir uma conexão SMTP real, porque a dependência é construída internamente.
  • Configuração espalhada. Detalhes de infraestrutura ficam enterrados dentro da regra de negócio.

A injeção de dependência ataca exatamente esse acoplamento.

O que "acoplamento" significa aqui

Vale precisar o termo, porque ele é o centro de tudo. Acoplamento é o grau em que um componente depende dos detalhes internos de outro. No exemplo acima, ServicoDeNotificacao está acoplado a uma classe concreta (ClienteSMTP) e até aos seus parâmetros de construção. Esse é o tipo mais rígido de dependência: para reaproveitar, testar ou evoluir a classe, você é obrigado a arrastar o ClienteSMTP junto. O objetivo da DI não é eliminar dependências — todo sistema útil tem dependências — mas trocar uma dependência rígida de implementação por uma dependência flexível de contrato.

O que é injeção de dependência?

Injeção de dependência (Dependency Injection, ou DI) é uma técnica em que um objeto recebe as dependências de que precisa, em vez de criá-las internamente. Quem fornece essas dependências é um agente externo — o "injetor", que pode ser código manual ou um container especializado.

Reescrevendo o exemplo com DI:

interface CanalDeEnvio {
  enviar(destino: string, mensagem: string): void
}

class ServicoDeNotificacao {
  // a dependência é recebida pelo construtor
  constructor(private canal: CanalDeEnvio) {}

  notificar(usuario: Usuario, mensagem: string) {
    this.canal.enviar(usuario.email, mensagem)
  }
}

Agora ServicoDeNotificacao não sabe — e não precisa saber — se o canal é SMTP, SMS ou um stub de teste. Ele depende de uma abstração (CanalDeEnvio), e a implementação concreta é injetada de fora:

const servico = new ServicoDeNotificacao(new CanalSMTP())
// ou, no teste:
const servico = new ServicoDeNotificacao(new CanalFalso())

O detalhe que muda tudo: depender da abstração

Repare no que aconteceu. Antes, a classe dependia de ClienteSMTP, uma implementação concreta. Agora ela depende de CanalDeEnvio, uma interface — um contrato que diz "qualquer coisa capaz de enviar(destino, mensagem) serve". A injeção em si (receber pelo construtor) é só metade da técnica; a outra metade é depender de uma abstração em vez de uma classe concreta. Sem a interface, você até poderia receber um ClienteSMTP pelo construtor, mas continuaria preso àquela implementação. É a combinação injeção + abstração que produz o desacoplamento real.

Inversão de controle: o princípio por trás

A injeção de dependência é uma forma concreta de aplicar um princípio mais amplo chamado inversão de controle (Inversion of Control, ou IoC).

Em um fluxo tradicional, seu código decide quando e como criar e chamar suas colaborações. Com inversão de controle, essa responsabilidade é invertida: um agente externo decide quais implementações serão usadas e as fornece ao seu código. Você inverte o controle sobre a criação e a ligação das dependências.

É importante não confundir os termos:

  • Inversão de controle (IoC) é o princípio geral: o controle sobre o fluxo e as dependências é entregue a um agente externo.
  • Injeção de dependência (DI) é uma das técnicas para implementar IoC — especificamente, fornecer dependências de fora.

A semente teórica dessa ideia é antiga. Bertrand Meyer já discutia, no contexto da construção orientada a objetos, a importância de projetar módulos que dependam de abstrações e contratos bem definidos em vez de implementações concretas (Meyer, 1988) — base conceitual do desacoplamento que a DI viabiliza.

O "princípio de Hollywood"

Uma forma intuitiva de entender IoC é o chamado Hollywood Principle: "não nos ligue, nós ligamos para você". Em código tradicional, você chama as bibliotecas. Com inversão de controle, o framework chama o seu código nos momentos certos, e você apenas fornece as peças que ele vai orquestrar. Frameworks de interface, servidores web e containers de DI funcionam todos assim: eles seguram o fluxo principal e invocam suas funções e objetos quando necessário. A DI é a fatia desse princípio que trata especificamente de quem cria e fornece as dependências.

Os três tipos de injeção

Há três formas clássicas de injetar dependências.

1. Injeção via construtor

As dependências são passadas como argumentos do construtor. É a forma mais recomendada porque torna as dependências obrigatórias e explícitas: o objeto não pode ser criado em estado inválido.

class ProcessadorDePedido:
    def __init__(self, repositorio, gateway_pagamento):
        self.repositorio = repositorio
        self.gateway_pagamento = gateway_pagamento

A grande vantagem é que a assinatura do construtor vira uma lista honesta de dependências: basta olhar para ela e você sabe exatamente do que a classe precisa. Se um construtor começa a pedir oito, dez dependências, isso é um sinal de alerta de que a classe faz coisas demais — um cheiro de código que a própria DI ajuda a tornar visível.

2. Injeção via setter/propriedade

A dependência é fornecida por um método ou propriedade após a construção. Útil para dependências opcionais, mas tem a desvantagem de permitir objetos temporariamente incompletos.

processador = ProcessadorDePedido()
processador.set_logger(logger)

O risco aqui é o objeto existir em um estado parcialmente montado: se alguém chamar um método antes de o setter ser invocado, a dependência estará ausente. Por isso a injeção via setter deve ficar reservada ao que é genuinamente opcional, como um logger que pode simplesmente não fazer nada se ausente.

3. Injeção via interface/método

O objeto implementa uma interface através da qual recebe a dependência. É menos comum, mas aparece em alguns frameworks.

Na prática, a injeção via construtor é a escolha padrão na maioria dos casos por sua clareza e segurança.

DI e o princípio da inversão de dependência

A injeção de dependência caminha lado a lado com o "D" do SOLID: o princípio da inversão de dependência (Dependency Inversion Principle). Ele afirma que:

  • Módulos de alto nível não devem depender de módulos de baixo nível; ambos devem depender de abstrações.
  • Abstrações não devem depender de detalhes; detalhes devem depender de abstrações.

A DI é o mecanismo prático que realiza esse princípio. Ao injetar uma interface em vez de uma classe concreta, o módulo de alto nível (a regra de negócio) passa a depender de uma abstração, e a implementação concreta (o detalhe) é fornecida de fora.

Essa combinação é justamente o que faz a regra de dependência da Arquitetura Limpa funcionar. Como Robert C. Martin descreve, ao inverter dependências por meio de abstrações conseguimos que o fluxo de controle vá em uma direção enquanto as dependências de código-fonte apontam na direção oposta (Martin, 2017) — exatamente o que protege o núcleo do sistema dos detalhes externos.

Onde a abstração deve "morar"

Há uma sutileza arquitetural que muita gente erra. A interface CanalDeEnvio não pertence ao módulo de baixo nível (o que implementa SMTP); ela pertence ao módulo de alto nível, que a define segundo suas próprias necessidades. O detalhe (o cliente SMTP) é que implementa o contrato ditado pelo núcleo. É essa inversão de quem "manda" no contrato que faz a dependência de código-fonte apontar do detalhe para a regra de negócio, e não o contrário. Quando a interface fica no lugar certo, você pode trocar toda a infraestrutura sem tocar no núcleo.

O composition root: onde tudo se conecta

Se as classes não criam suas dependências, alguém precisa criar. Esse "alguém" deve ficar concentrado em um único lugar: o composition root (raiz de composição), tipicamente no ponto de entrada da aplicação (a função main, o bootstrap do servidor).

// composition root: o único lugar que conhece as implementações concretas
function bootstrap() {
  const canal = new CanalSMTP('smtp.exemplo.com', 587)
  const repositorio = new RepositorioPostgres(conexao)
  const gateway = new GatewayStripe(chaveApi)

  const notificacao = new ServicoDeNotificacao(canal)
  const pedidos = new ProcessadorDePedido(repositorio, gateway)

  return new Aplicacao(notificacao, pedidos)
}

A ideia é poderosa: todo o conhecimento sobre quais implementações concretas usar fica em um único ponto. O resto do sistema só conhece abstrações. Quer trocar o Postgres por um banco em memória? Muda uma linha no composition root. Quer rodar em modo de teste? Monta um composition root alternativo. Concentrar a "fiação" do sistema em um lugar é o que mantém o desacoplamento gerenciável conforme a aplicação cresce.

Containers de injeção de dependência

Em sistemas pequenos, você pode "injetar à mão", instanciando e conectando objetos manualmente no composition root. À medida que o grafo de dependências cresce, isso fica trabalhoso, e é aí que entram os containers de DI (ou IoC containers).

Um container é uma ferramenta que sabe como construir os objetos e resolve automaticamente suas dependências. Você registra os mapeamentos (abstração → implementação) e o container monta o grafo para você.

// Exemplo conceitual de registro em um container
container.register('CanalDeEnvio', CanalSMTP)
container.register('ServicoDeNotificacao', ServicoDeNotificacao)

// O container resolve a cadeia de dependências automaticamente
const servico = container.resolve('ServicoDeNotificacao')

Exemplos no ecossistema incluem Spring (Java), o sistema de DI nativo do Angular e do NestJS (TypeScript), e bibliotecas como inversify. Eles também gerenciam o ciclo de vida dos objetos.

Ciclos de vida: singleton, scoped e transient

Um container não decide apenas o que injetar, mas quanto tempo cada objeto vive. Os três tempos de vida clássicos são:

  • Singleton: uma única instância para toda a aplicação. Ideal para objetos sem estado e caros de criar, como pools de conexão ou clientes HTTP.
  • Scoped (por requisição): uma instância por requisição ou unidade de trabalho. Comum em aplicações web, onde cada requisição quer seu próprio contexto de banco.
  • Transient: uma nova instância a cada vez que é solicitada. Para objetos leves e descartáveis.

Errar o ciclo de vida é uma fonte clássica de bugs sutis. Registrar como singleton algo que guarda estado por requisição, por exemplo, faz dados de um usuário vazarem para outro — um bug difícil de reproduzir e potencialmente grave em segurança.

Vale o alerta: containers são úteis em sistemas grandes, mas adicionam complexidade e "mágica". Em projetos menores, a injeção manual costuma ser mais clara e suficiente.

Por que DI melhora a testabilidade

Talvez o maior benefício prático da DI seja o impacto nos testes automatizados. Como as dependências são fornecidas de fora, é trivial substituí-las por dublês (mocks, stubs, fakes) durante os testes.

def test_pedido_recusado_quando_pagamento_falha():
    gateway_falso = GatewayQueSempreRecusa()
    repositorio_falso = RepositorioEmMemoria()
    processador = ProcessadorDePedido(repositorio_falso, gateway_falso)

    resultado = processador.processar(pedido_qualquer)

    assert resultado.status == "recusado"

Sem DI, esse teste precisaria de um gateway de pagamento real e de um banco de dados de verdade. Com DI, você controla totalmente o ambiente do teste, tornando-o rápido, determinístico e isolado.

Os tipos de dublê de teste

Como a DI permite substituir dependências, vale conhecer o vocabulário dos substitutos:

  • Stub: devolve respostas fixas e pré-programadas (um RepositorioEmMemoria que sempre retorna o mesmo usuário).
  • Mock: além de responder, verifica se foi chamado da forma esperada (quantas vezes, com quais argumentos).
  • Fake: uma implementação simplificada mas funcional (um banco em memória de verdade, em vez do Postgres).

A escolha entre eles depende do que o teste quer verificar. O importante é que a DI é o que torna todos eles plugáveis sem gambiarra: como o objeto recebe a dependência de fora, basta passar o dublê adequado.

Benefícios e custos

Benefícios:

  • Desacoplamento. Componentes dependem de abstrações, não de implementações concretas.
  • Testabilidade. Dependências são facilmente substituídas em testes.
  • Flexibilidade. Trocar uma implementação não exige alterar quem a usa.
  • Configuração centralizada. A montagem do sistema fica concentrada no composition root.

Custos:

  • Indireção. Pode ficar menos óbvio qual implementação concreta está sendo usada.
  • Curva de aprendizado de containers e de seus conceitos de ciclo de vida.
  • Excesso de abstração. Criar interfaces para tudo, mesmo quando há uma única implementação, gera burocracia desnecessária.

A boa prática é injetar dependências que variam ou que representam fronteiras importantes (banco, rede, serviços externos), sem transformar cada detalhe trivial em uma abstração.

Erros comuns ao aplicar DI

  • Abstrair o que nunca vai mudar. Criar uma interface IUsuario para uma estrutura de dados simples não desacopla nada; só adiciona ruído.
  • O Service Locator disfarçado. Em vez de receber dependências, a classe pede ao container ("me dê um CanalDeEnvio"). Isso reintroduz acoplamento — agora ao container — e esconde as dependências reais. Prefira receber, não buscar.
  • Construtores gigantes ignorados. Um construtor com muitas dependências é um diagnóstico, não um inconveniente: a classe provavelmente viola a responsabilidade única. Não esconda o problema com um setter; quebre a classe.
  • Ciclo de vida errado. Compartilhar como singleton algo que guarda estado por requisição causa vazamento de dados entre usuários.
  • DI sem abstração. Injetar uma classe concreta pelo construtor é melhor que instanciá-la internamente, mas só desacopla de verdade quando você depende de uma interface.

Perguntas frequentes

DI e IoC são a mesma coisa? Não. IoC é o princípio amplo de entregar o controle a um agente externo; DI é uma técnica específica para realizar IoC, focada em fornecer dependências de fora. Toda DI é IoC, mas IoC inclui outras ideias.

Preciso de um framework para usar DI? Não. DI é, antes de tudo, um padrão de design: basta receber as dependências pelo construtor e montá-las no composition root. Containers e frameworks só automatizam isso em sistemas grandes.

DI deixa o código mais lento? O custo em tempo de execução é desprezível na maioria dos casos. O custo real é de indireção e legibilidade, não de performance.

Qual tipo de injeção devo usar? Construtor, na imensa maioria dos casos, porque torna as dependências obrigatórias e explícitas. Reserve o setter para dependências genuinamente opcionais.

Devo criar uma interface para cada classe? Não. Crie abstrações onde há variação real ou fronteiras importantes (banco, rede, serviços externos). Interface para tudo é excesso de abstração e vira burocracia.

Conclusão

Injeção de dependência é, no fundo, uma ideia simples: não deixe um objeto criar suas próprias dependências — entregue-as a ele. Essa pequena mudança, sustentada pelo princípio da inversão de controle e potencializada por depender de abstrações, produz código mais desacoplado, mais testável e mais fácil de evoluir. Prefira a injeção via construtor, concentre a montagem no composition root, escolha o ciclo de vida certo e use containers apenas quando a complexidade justificar. Cuidado com os exageros — abstrair o que não varia ou disfarçar um service locator de DI traz mais ruído que benefício. Bem aplicada, a DI é uma das técnicas mais eficazes para manter um sistema flexível ao longo do tempo, e é a engrenagem que faz arquiteturas em camadas e o princípio da inversão de dependência saírem da teoria para a prática.

Referências

  • Meyer, B. (1988). Object-Oriented Software Construction. Prentice Hall.
  • Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to Software Structure and Design. Prentice Hall.
  • Fowler, M. (2004). Inversion of Control Containers and the Dependency Injection pattern. martinfowler.com.
  • Seemann, M., van Deursen, S. (2019). Dependency Injection Principles, Practices, and Patterns. Manning.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly