Introdução
Sempre ouvimos dizer que no nosso código precisamos buscar diminuir o acoplamento e aumentar a coesão. Pra alcançar esse fim existem princípios que guiam o desenvolvedor na hora da criação do código. Esses princípios são bastante explorados em diversos posts — o famoso S.O.L.I.D. — e hoje eu trago um pequeno resumo da Inversão de Dependência, o D desse acrônimo.
A principal consequência de aplicar a técnica de Inversão de Dependência é a obtenção de um código mais desacoplado e mais facilmente testável. A seguir, busco explicar os motivos.
O contexto
A figura abaixo é retirada do livro Arquitetura Limpa, onde o autor introduz o tema pela primeira vez.
- MAINFunção principal
- HLHigh-level function
- MLMid-level function
- LLLow-level function
- Seta verde tracejadaControle de fluxo
- Seta vermelha contínuaDependência de código
“Beleza, entendi a imagem. Mas o que especificamente quer dizer controle de fluxo e dependência?”
Você pode se perguntar. E eu resumo assim:
Controle de fluxo
É a ordem na qual as instruções são chamadas. Pode-se dizer que o fluxo é o caminho que a thread de execução percorre — é o caminho que a execução do programa segue, determinado por loops, estruturas de decisão, chamadas de funções e outros mecanismos de controle de fluxo.
Dependência de código
Diz-se que há dependência entre códigos quando há a necessidade de importação de um código em outro. No exemplo acima, o módulo MAIN precisa ter uma referência de HL (precisa importar HL) para funcionar.
Na figura acima, temos que o fluxo do código está atrelado às dependências, ou seja, MAIN só pode chamar uma função de HL (ou seja, direcionar o controle de fluxo) pois existe uma importação de HL em MAIN.
Isso gera uma dependência, de forma que MAIN depende de HL para funcionar. Essa dependência quebra o princípio de Inversão de Dependência, que diz que módulos de alto nível não devem depender de módulos de baixo nível — ambos devem depender de abstrações (interfaces).
Dependendo de interfaces
Interfaces funcionam como um contrato entre os envolvidos. Uma classe (ou uma função, como veremos) que implementa uma interface significa que aquele que implementa deve possuir todos os métodos e atributos que a interface possui.
Por exemplo:
- Seta vermelha simples (HL1 → I)Dependência de USO
- Seta vermelha de diamante aberto (ML1 → I)Dependência de IMPLEMENTAÇÃO
- Seta verde tracejadaControle de fluxo
Aqui, vale notar que as setas vermelhas são diferentes:
- Seta vermelha de HL1 para I: indica que o módulo HL1 depende da interface I para chamar a função F(). Isso significa que o código em HL1 está escrito para interagir com a interface I, não diretamente com a implementação específica em ML1. Essa dependência na interface permite que HL1 seja mais flexível e desacoplado das implementações específicas de ML1.
- Seta vermelha de ML1 para I: indica que o módulo ML1 implementa a interface I. Em outras palavras, ML1 fornece a implementação concreta da função F() definida pela interface I. Isso permite que diferentes implementações de I possam ser usadas sem que HL1 precise mudar, promovendo o princípio da inversão de dependência.
Por isso é tão importante que a função que implementa uma interface implemente todos os métodos dela — outras funções esperam que isso aconteça (além disso, o TypeScript vai gritar com você se não o fizer).
Mas onde está a inversão, afinal?
Olhe novamente para a imagem acima, onde temos a interface. Nesse momento, a inversão de dependência já aconteceu. Podemos reorganizar essa imagem da seguinte forma, para melhor visualização:
Como durante o runtime a interface não existe, HL1 não depende da interface — ele a usa. Durante o runtime, HL1 simplesmente chama a função F() disponível em ML1 sem precisar importar ML1 diretamente.
Perceba que o controle de fluxo e a dependência de código (ML1 e I) estão apontando para sentidos opostos, caracterizando uma inversão de dependência.
Como isso fica na prática?
Enquanto eu estava revisando esse assunto, encontrei uma excelente e bem pequena explicação prática sobre Inversão de Dependência com TypeScript. Então, ao invés de criar minha versão me inspirando (copiando 😅), acho mais interessante compartilhar a fonte aqui direto.
Esse artigo do Alex Nault tem um exemplo bem bacana, simples e sucinto sobre inversão com TypeScript. Acredito que o conteúdo desse meu artigo complementa muito o dele, e vice-versa.
Em seu artigo, Alex demonstra uma implementação sem DIP e depois demonstra como aplicar DIP no mesmo contexto. Além disso, a explicação dele sobre como essa prática aumenta a testabilidade é muito boa.
Próximos passos
Se você leu o artigo do Alex Nault, percebeu que ele mostra as entidades envolvidas, mas não coloca os relacionamentos entre elas. Por exemplo:
Aqui, o autor apenas mostrou as entidades com as quais vai trabalhar, mas não estabeleceu graficamente os relacionamentos.
Pegue papel e caneta (ou o Excalidraw), replique essas entidades e tente aplicar os relacionamentos. De onde sai e para onde aponta o fluxo de controle? E a seta que indica uso? E a seta que indica implementação?
Pode parecer bobo, mas acredito que isso pode te ajudar a consolidar o conceito.
Referências
- MARTIN, Robert C. Clean Architecture: A craftsman's guide to software structure and design. Prentice Hall, 2017.
- MENDES, Antonio. Arquitetura de Software: Desenvolvimento orientado para arquitetura. Editora Campus, 2002.
- NAULT, Alex. Dependency inversion principle in functional TypeScript. Disponível em: alexnault.dev/dependency-inversion-principle-in-functional-typescript. Acesso em: 2 ago. 2024.
Precisa de ajuda com a arquitetura do seu projeto?
Desenvolvo sites e produtos digitais com foco em código sustentável. Vamos conversar sobre o seu.
Falar comigo