Photo by Artem Podrez on pexels
Chegamos à última letra do acrônimo SOLID: o D, de Dependency Inversion Principle (Princípio da Inversão de Dependência). A regra é clara (ou não):
- 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 (implementações concretas) devem depender de abstrações.
Mas por que devemos nos lembrar deste princípio?
Muitos desenvolvedores vão dizer que é para melhorar a testabilidade, manutenibilidade, melhorar a modularização etc.
E estão certíssimos!
Mas irei explicar a importância deste princípio em termos financeiros, ou seja, de custo!
Pois nós queremos algo testável, manutenível e modularizado pois o custo de alteração fica bem menos custoso!
Então vamos olhar um exemplo prático onde o DIP é ignorado e o que acontece quando invertemos a direção da dependência.
Uso de biblioteca#
É muito comum nós utilizarmos bibliotecas nos projetos para facilitar a nossa vida de escrever código.
Um que ficou muito famoso no mundo java foi o joda-time
.
Antes do Java 8, trabalhar com a API de datas e horas era muito mais simples com o joda-time
. Algumas vantagens:
// Joda-Time tem Fluent API
LocalDate date = LocalDate.of(2014, 12, 25); // Somente ano, mes dia
LocalTime time = LocalTime.of(14, 30); // Somente hora, minuto
LocalDateTime dt = LocalDateTime.now(); // Data e hora atuais
// Java pré-8
Date oldDate = new Date(); // Tem todos os componentes
Logo, era comum então muitas classes terem dependência direta para as classes do joda-time
:
class BusinessRuleOne {
public void doSomeWork() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime oneWeekLater = now.plusWeeks(1);
Duration interval = Duration.between(now, oneWeekLater);
// Do some work with the interval
}
}
class BusinessRuleTwo {
public void doMoreWork() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime sixHoursLater = now.plusHours(6);
Duration interval = Duration.between(now, sixHoursLater);
// Do some work with the interval
}
}
Legal! Mas a nova versão Java 8
trouxe melhorias significativas na API de datas, tornando praticamente desnecessário o uso da biblioteca joda-time
.
Era uma notícia boa!
Agora imaginem a seguinte situação. Seu projeto usa o joda-time
. É um projeto grande. Pelo menos 70 classes utilizam joda-time
.
O custo para tirar o joda-time
e utilizar a API do Java 8
é altíssimo! Estes custos por classe são:
- 💰 Refatoração dos imports
- 💰 Verificar se existe um conceito relacionado na nova API (Para os curiosos, usei este blog como consulta: Converting joda-time to java.time)
- 💰 Garantia de que a classe continua funcionando de maneira esperada (Neste passo, o custo aumenta muito, especialmente se não existirem testes unitários)
- 💰 Realizar testes manuais para cada alteração feita (Muitas equipes pulam este passo, exatamente pelo custo alto)
Como temos 70 classes, o custo seria de 70 x 4 x 💰
.
Esse é um exemplo clássico mas pouco conhecido e reconhecido da consequência da quebra do DIP.
Como poderíamos ter evitado este custo alto? Nós poderíamos ter aplicado o DIP no uso da biblioteca!
Para ilustrar melhor o problema:
flowchart TB; subgraph A [Módulo de Alto Nível] BRO[BusinessRuleOne] BRT[BusinessRuleTwo] M[...mais 68 classes] end subgraph B [Módulo de Baixo Nível] JT[Joda Time] end A e1@== depende de ==> B; e1@{ animate: true }
Como queremos eliminar o joda-time
do projeto, temos que cortar a dependência diretamente de 70 classes.
Aplicando o DIP#
Para evitar este alto acoplamento, poderíamos ter isolado o uso do joda-time
da seguinte maneira:
flowchart TB; subgraph A [Módulo de Alto Nível] direction TB; subgraph CORE[Regras de negócio] BRO[BusinessRuleOne] BRT[BusinessRuleTwo] M[...mais 68 classes] end subgraph ABS[Abstração] ACL[DateACL] end CORE ==> ABS end subgraph B [Módulo de Baixo Nível] JT[Joda Time] end B e1@== depende de ==> ABS; e1@{ animate: true }
Conseguimos “inverter” a direção da dependência criando uma abstração (interface do Java) chamada de DateACL
.
Assim as regras de negócio dependem da abstração e o módulo de baixo nível (implementação) é forçado a cumprir o contrato definido em DateACL
.
Por isso este princípio tem esse nome. Pois invertemos a dependência.
- Antes do
DIP
: Alto nível – depende de –> Baixo nível - Depois do
DIP
: Baixo nível – depende de –> Abstração presente no alto nível
O código refatorado seria assim:
A abstração:
interface DateACL {
String now();
String getInterval(String start, String end);
String plusWeeks(String date, int weeks);
String plusHours(String date, int hours);
}
As classes de serviço:
class BusinessRuleOneDIP {
private final DateACL dateAPI;
// Injeção de dependência via construtor
public BusinessRuleOneDIP(DateACL dateAPI) {
this.dateAPI = dateAPI;
}
public void doSomeWork() {
// A regra de negócio não depende mais da implementação concreta da API de datas
String now = dateAPI.now();
String oneWeekLater = dateAPI.plusWeeks(now, 1);
String interval = dateAPI.getInterval(now, oneWeekLater);
}
}
class BusinessRuleTwoDIP {
private final DateACL dateAPI;
// Injeção de dependência via construtor
public BusinessRuleTwoDIP(DateACL dateAPI) {
this.dateAPI = dateAPI;
}
public void doMoreWork() {
String now = dateAPI.now();
String sixHoursLater = dateAPI.plusHours(now, 6);
String interval = dateAPI.getInterval(now, sixHoursLater);
}
}
A implementação no módulo de baixo nível:
class DateAPIWithJodaTime implements DateACL {
@Override
public String now() {
return org.joda.time.LocalDateTime.now()
.toString();
}
@Override
public String getInterval(String start, String end) {
org.joda.time.LocalDateTime jodaStart = new org.joda.time.LocalDateTime(start);
org.joda.time.LocalDateTime jodaEnd = new org.joda.time.LocalDateTime(end);
return new Interval(jodaStart.toDateTime(), jodaEnd.toDateTime()).toString();
}
@Override
public String plusWeeks(String date, int weeks) {
return new org.joda.time.LocalDateTime(date).plusWeeks(weeks)
.toString("yyyy-MM-dd'T'HH:mm");
}
@Override
public String plusHours(String date, int hours) {
return new org.joda.time.LocalDateTime(date).plusHours(hours)
.toString("yyyy-MM-dd'T'HH:mm");
}
}
E a vantagem deste novo design é que o custo para eliminar o joda-time
é simplesmente implementar a DateACL com a API do Java 8
.
DIP possibilita a antiga implementação e a nova implementação existir ao mesmo tempo#
A nova implementação do módulo de baixo nível:
class DateAPIWithJava8Native implements DateACL {
@Override
public String now() {
return LocalDateTime.now()
.toString();
}
@Override
public String getInterval(String start, String end) {
return Duration.between(LocalDateTime.parse(start), LocalDateTime.parse(end))
.toString();
}
@Override
public String plusWeeks(String date, int weeks) {
return LocalDateTime.parse(date)
.plusWeeks(weeks)
.toString();
}
@Override
public String plusHours(String date, int hours) {
return LocalDateTime.parse(date)
.plusHours(hours)
.toString();
}
}
Ilustrado, ficaria algo assim:
flowchart TB; subgraph A [Módulo de Alto Nível] direction TB; subgraph CORE[Regras de negócio] BRO[BusinessRuleOne] BRT[BusinessRuleTwo] M[...mais 68 classes] end subgraph ABS[Abstração] ACL[DateACL] end CORE ==> ABS end subgraph B [Módulo de Baixo Nível] JT[Joda Time] NATIVE[Java 8 Native] end B e1@== depende de ==> ABS; e1@{ animate: true }
Vendo a ilustração conseguimos perceber mais uma vantagem: Temos as duas implementações!
Ou seja, não precisamos “deleter a implementação antiga”. As duas implementações podem estar presentes no código! Para dar rollback é muito menos custoso.
Então resumindo o custo para eliminar o joda-time
com o novo design seria:
- 💰 Criar uma nova implementação do
DateACL
- 💰 Verificar se o comportamento se mantem com teste unitário
E só! Nós não precisamos verificar manualmente o resto do sistema que depende do DateACL
pois só precisamos garantir que o comportamento da abstração continua inalterado.
Ou seja, em termos de custo, é 70 x 4 x 💰
versus 2 x 💰
.
Este é o benefício de um DIP bem aplicado.
Então fica a dica:
DIP serve como fachada e parte do glossário do seu sistema#
Para aplicar o DIP
, normalmente precisamos criar uma abstração.
Esta abstração é criada de acordo com a necessidade do sistema.
Ou seja, fica documentado o comportamento esperado que queremos da biblioteca!
E também evitamos que conceitos ou implementações da biblioteca “vazem” para dentro do nosso sistema.
Serve como uma fachada!
DIP facilita a criação dos testes#
Já que a abstração serve como glossário ou parte de uma linguagem ubíqua do seu sistema, podemos criar testes unitários para testar as implementações!
E quando temos o teste unitário, é muito rápido e fácil testar várias implementações ao mesmo tempo e comparar o resultado.
Por exemplo:
@ParameterizedTest
@ValueSource(classes = {DateAPIWithJava8Native.class, DateAPIWithJodaTime.class})
void plusWeeksTest(Class<DateACL> dateACLClass) throws NoSuchMethodException,
InvocationTargetException,
InstantiationException,
IllegalAccessException {
var dateACL = dateACLClass.getDeclaredConstructor()
.newInstance();
String result = dateACL.plusWeeks("2025-10-06T10:00:00", 7);
Assertions.assertThat(result).isEqualTo("2025-11-24T10:00");
}
Resumo sobre DIP#
Vamos resumir os conceitos relacionados com o DIP!
Módulo Alto Nível#
Fornece abstrações sobre funcionalidades e comportamentos do sistema. Não deve se comunicar diretamente com módulos de baixo nível.
Ex: Regras de negócio
Módulo de Baixo Nível#
Fornece implementações concretas (banco de dados, fila e etc) e deve atender a abstração disponível no módulo de alto nível.
Ex: Infraestrutura
Quebra do DIP#
flowchart LR; subgraph A [Módulo de Alto Nível] end subgraph B [Módulo de Baixo Nível] end A e1@== depende de ==> B; e1@{ animate: true }
DIP aplicado#
flowchart LR; subgraph A [Módulo de Alto Nível] Abstração end subgraph B [Módulo de Baixo Nível] end B e1@== depende de ==> A; e1@{ animate: true }
Conclusão#
Aplicar o Dependency Inversion Principle
significa prevenir-se de alto custo de manutenção.
Adotar o Princípio da Inversão de Dependência é uma decisão tática que protege o projeto de custos futuros.
Ao investir em abstrações bem pensadas, criamos um sistema flexível onde a troca de componentes se torna uma tarefa de baixo risco, garantindo que a arquitetura evolua com o negócio, em vez de se tornar um obstáculo.
O que você achou do artigo?
Se tiver mais curiosidade sobre o assunto, venha falar comigo!
🥒🥒 Espero que tenham curtido e até o próximo artigo! 🥒🥒