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 Schematize Blog ·

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.

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:

    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:

      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:

        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:

          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:

            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:

              Custos:

                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

                  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

                    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