Joao Brietzke Blog

← voltar

Dependency Inversion Principle (DIP)

O seu código não deve depender da implementação de uma classe de nível mais baixo. Deve depender de um contrato.


A ideia

Basicamente: o nosso código não deve depender da implementação de classes de nível mais baixo. Em vez disso, a gente depende de contratos e abstrações.

O caso de uso não conhece o Postgres, não conhece o provedor de e-mail, não conhece a API do gateway de pagamento. Ele conhece um contrato que diz o que precisa acontecer, e alguém de fora entrega uma implementação que cumpre aquilo.

Por que é a regra que eu mais uso

Essa é a que eu mais aplico no dia a dia, e o motivo é a arquitetura.

A gente usa Clean Architecture para separar as camadas do código, e o DIP é o que faz essa separação existir de verdade. Sem ele as camadas são só pastas com nomes bonitos: o caso de uso continua importando o cliente do banco de dados direto, e a fronteira não segura nada.

Dois ganhos concretos, que são os que me fazem usar isso sempre:

  • Testar fica fácil. Como a classe recebe o contrato, no teste eu entrego uma implementação de mentira e pronto. Não preciso subir banco, não preciso de rede, não preciso de mock de biblioteca.
  • Trocar o adapter fica isolado. A implementação concreta vira um detalhe plugado na borda. Trocar o provedor mexe no adapter, e o miolo do sistema não fica sabendo.

O mecanismo é sempre o mesmo: injetar o contrato na classe, em vez da implementação direta.

Na prática o que eu faço é criar um contrato para conversar com aquilo. Um repositório de usuário, por exemplo: o contrato diz o que dá para fazer com usuário, e eu posso usar qualquer banco por trás, desde que ele respeite o contrato. A classe que consome integra com o contrato e nunca com o banco.

DIP não é injeção de dependência

Essas três coisas vivem sendo tratadas como sinônimo, e não são:

O que é
DIP A regra de direção: depender de abstração, e a abstração pertence à camada de cima.
Injeção de dependência O mecanismo de entrega: construtor, setter, parâmetro.
Container de IoC A ferramenta que automatiza a fiação.

Dá para fazer injeção sem DIP: se você recebe RepositorioPostgres pelo construtor, injetou, e continuou dependendo da implementação. O construtor não salva ninguém; o que salva é o tipo do parâmetro ser o contrato.

E dá para fazer DIP sem container nenhum, montando as dependências na mão no ponto de entrada da aplicação. Container é conveniência, não é o princípio.

De onde vem a palavra "inversão"

Vale entender o que exatamente está sendo invertido, porque o nome não é óbvio.

Num sistema em camadas tradicional, a dependência segue o fluxo de execução. O caso de uso chama o repositório, então o caso de uso importa o repositório:

camada de cima            camada de baixo
┌───────────┐   importa   ┌─────────────────────┐
│ CasoDeUso │ ──────────→ │ RepositorioPostgres │
└───────────┘             └─────────────────────┘

O problema é que a seta aponta da política para o detalhe. Quem manda no sistema passa a depender de quem devia ser plugável.

Com o DIP, o contrato passa a pertencer à camada de cima, e é a implementação que se curva para atendê-lo:

camada de cima
┌───────────┐  depende de  ╔══════════════════════╗
│ CasoDeUso │ ───────────→ ║ RepositorioDeUsuario ║  ← o contrato
└───────────┘              ╚══════════════════════╝
                                      ↑
                                      │ implementa
camada de baixo            ┌──────────┴──────────┐
                           │ RepositorioPostgres │
                           └─────────────────────┘

O fluxo de execução continua igual: o caso de uso ainda chama o Postgres em runtime. O que virou do avesso foi a dependência de código-fonte: agora ela aponta contra o fluxo. Essa é a inversão.

E daí sai o detalhe que decide se o DIP foi aplicado ou não: o contrato pertence a quem consome, não a quem implementa. Se a interface mora junto do adapter e é escrita espelhando o que aquele adapter sabe fazer, você não inverteu nada, só acrescentou um arquivo no meio do caminho. É a mesma regra que aparece no ISP: a interface é do cliente.

Como fica

Um caso de uso de criar usuário, em TypeScript.

Antes: o caso de uso instancia o banco dentro de si

class CriarUsuario {
  // A dependência nasce aqui dentro. Quem consome o caso de uso não tem como
  // interferir: para rodar isto, é Postgres ou nada.
  private repositorio = new RepositorioPostgres();

  exec(email: string): string {
    if (this.repositorio.buscarPorEmail(email) !== null) {
      return `✗ e-mail já cadastrado: ${email}`;
    }
    this.repositorio.salvar({ email });
    return `Usuário criado: ${email}`;
  }
}

Não dá para testar sem um banco de pé, e trocar o adapter significa editar o caso de uso, que é justamente o código que não devia mudar por causa de infraestrutura.

Depois: o caso de uso recebe o contrato

// O contrato pertence à camada de cima: diz o que o caso de uso precisa,
// não o que um banco específico sabe fazer. Repare que não tem SQL aqui.
interface RepositorioDeUsuario {
  buscarPorEmail(email: string): Usuario | null;
  salvar(usuario: Usuario): void;
}

class CriarUsuario {
  private repositorio: RepositorioDeUsuario;

  // O que caracteriza o DIP é o TIPO do parâmetro ser o contrato.
  // Vir pelo construtor é só o mecanismo.
  constructor(repositorio: RepositorioDeUsuario) {
    this.repositorio = repositorio;
  }

  exec(email: string): string {
    if (this.repositorio.buscarPorEmail(email) !== null) {
      return `✗ e-mail já cadastrado: ${email}`;
    }
    this.repositorio.salvar({ email });
    return `Usuário criado: ${email}`;
  }
}

// Adapters plugados na borda. O caso de uso não conhece nenhum dos dois.
class RepositorioPostgres implements RepositorioDeUsuario { /* fala SQL */ }
class RepositorioEmMemoria implements RepositorioDeUsuario { /* guarda num Map */ }

O exec não mudou uma linha. O que mudou foi de onde vem o repositório, e com isso o teste passa a ser new CriarUsuario(new RepositorioEmMemoria()), sem banco, sem rede e sem mock de biblioteca.

▸ Exemplo completo e executável

Em outras linguagens: o mesmo exemplo em Python e Java, com saída idêntica. Em Python o contrato é um Protocol, satisfeito estruturalmente: o adapter não declara que implementa nada, basta ter os métodos.

Princípios relacionados

  • Open/Closed (OCP): o OCP quer que comportamento novo entre sem editar o que já funciona; o DIP diz para onde a dependência precisa apontar para isso ser possível. A injeção que aparece lá é DIP em ação.
  • Interface Segregation (ISP): o ISP decide o formato do contrato, o que entra e o que fica de fora. O DIP decide de que lado ele mora e para onde a dependência aponta. Os dois concordam num ponto: o contrato é de quem consome.
  • Liskov Substitution (LSP): o DIP é o que torna a substituição possível, ao fazer o código depender do contrato. O LSP é o que diz se uma implementação específica pode mesmo ocupar aquele lugar sem quebrar quem chama.