Ir para o conteúdo principal
Background Image

Princípio Dependency Inversion Principle (DIP) na prática

·8 minutos·
Rafael Issao
Autor
Rafael Issao
Apaixonado por tecnologia, programação e inovação. Adoro compartilhar conhecimento e aprender coisas novas todos os dias.
Tabela de conteúdos
SOLID na prática - Este artigo faz parte de uma série de artigos.
Parte 5: Esse Artigo

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):

  1. Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.
  2. 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:

Dica! Ao usar qualquer biblioteca na sua aplicação, aplique o DIP! Fica muito mais fácil de trocar de biblioteca ou atualizar a versão principal (major) da biblioteca sem quebrar tanto o sistema!

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! 🥒🥒

SOLID na prática - Este artigo faz parte de uma série de artigos.
Parte 5: Esse Artigo