Posts SOLID - Dependency Inversion Principle
Post
Cancel

SOLID - Dependency Inversion Principle

SOLID: Dependency Inversion Principle (DIP)

Esse é meu princípio favorito e de muita gente, pois, nos guia para sistemas flexíveis e de fácil manutenção e evolução.

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

Depender de abstrações inverte o fluxo de dependência porque a classe concreta que passa a depender da abstração. Dependemos de abstrações, pois as mesmas tendem a ser menos voláteis, isso é, uma implementação pode mudar com novas features/fixes, mas abstrações são mais difíceis, tendem a ser mais estáveis. Mudanças em abstrações afetam todas as implementações, mas mudanças em uma implementação afeta apenas ela mesmo.

Entendendo Abstrações em Java

Se você está familiarizado com programação orientada a objetos (POO), provavelmente já entendeu de cara o significado, caso não, iremos discuti-lo em Java, especificamente. Todos os princípios SOLID estão correlacionados em algum nível com POO, mas esse em particular é o que mais carrega referencias.

Em POO, abstrações são classes que não podem ser instânciadas diretamente, e, em geral, são utilizadas para definir um comportamento em comum. Abstrações são usados em heranças com diferentes comportamentos dependendo da implementação (Polimorfismo). Como podem perceber, alguns pilares de POO estão envolvidos com DIP.

poo_pilares

Em Java, temos interface e abstract keywords (palavras reservadas). Interfaces são usadas para definir contratos para implementação, já Abstrações são classes que possuem algum comportamento padrão (default methods) que são comuns a qualquer implementação. Ambos interface e Abstract são utilizadas para atingir a abstração do POO.

Exemplo de interface:

1
2
3
4
5
package org.example;

public interface FormaPagamento {
  public abstract void processarPagamento();
}

Exemplo de abstract:

1
2
3
4
5
6
7
8
9
10
package org.example;

public abstract class FormaPagamento {
  public abstract void processarPagamento(String str);

  protected String sanitizarDocumento(String documento) {
    // Comportamento padrão necessário para todos FormaPagamento
    return documento.replaceAll("[.-]", "");
  }
}

No exemplo de abstract, temos um comportamento padrão para remover pontos e traços de um documento para processar o pagamento, necessário para todas as implementações ao enviar o documento (cpf ou cnpj) para a instituição financeira.

Implementando DIP

A implementação de DIP é bem simples, por norma, não se referir a classes, métodos, funções, etc concretas. Seguir essa regra a risca não é possível e seria inviável, pois, dependemos de algumas classes concretas estáveis no código que raramente mudam, como java.lang.String. A regra faz sentidos em módulos em ativo desenvolvimento e evolução, principalmente quando estamos falando de regras de negócio em empresas em geral. É aconselhável a não depender de frameworks e libs também, assim mantendo o projeto alinhado com DIP e arquitetura limpa. Nosso trabalho é evitar depender de módulos/implementações voláteis, e, permitir que os contratos abstratos sejam facilmente extensíveis, assim, alinhando até mesmo com Open Close Principle (OCP).

Exemplo de uma quebra de DIP:

1
2
3
4
5
public class PagamentoCredito {
  public Boolean processarPagamentoCredito(Dados dados) {
    // implementação credito
  }
}
1
2
3
4
5
public class PagamentoDebito {
  public Boolean processarPagamentoDebito(Dados dados) {
    // implementação debito
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ProcessadorPagamento {

  PagamentoCredito pagamentoCredito; // dependencia
  
  PagamentoDebito pagamentoDebito; // dependencia
  
  public ProcessadorPagamento(PagamentoDebito pagamentoDebito, PagamentoCredito pagamentoCredito) {
    this.pagamentoCredito = pagamentoCredito;
    this.pagamentoDebito = pagamentoDebito;
  }
  
  public void processarPagamento(Dados dados) {
    if (dados.getFormaPagamento() == 1) {
      pagamentoCredito.processarPagamentoCredito(dados);
    }
    if (dados.getFormaPagamento() == 2) {
      pagamentoDebito.processarPagamentoDebito(dados);
    }
  }
}

dip_dependencias

Acima vemos um caso claro de quebra de DIP, pois dependemos de duas classes concretas. Estamos quebrando OCP também, pois, caso uma nova forma de pagamento seja necessária, precisaríamos editar a classe ProcessadorPagamento diretamente.

Agora, veremos um exemplo de como se adequar ao DIP:

1
2
3
4
public interface FormaPagamento { 
  // Abstração
  Boolean processarPagamento(Dados dados);
}
1
2
3
4
5
6
public class PagamentoCredito implements FormaPagamento {
  @Override
  public Boolean processarPagamento(Dados dados) {
    // implementação credito
  }
}
1
2
3
4
5
6
public class PagamentoDebito implements FormaPagamento {
  @Override
  public Boolean processarPagamento(Dados dados) {
    // implementação debito
  }
}
1
2
3
4
public interface FormaPagamentoFactory { 
  // Abstract Factory
  FormaPagamento create();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class FormaPagamentoFactoryImpl {

  public enum FormaPagamentoEnum {
    CREDITO(1),
    DEBITO(2);

    private final int value;

    FormaPagamentoEnum(int value) {
      this.value = value;
    }

    public int getValue() {
      return value;
    }

    public static FormaPagamentoEnum fromValue(int value) {
      for (FormaPagamentoEnum method : FormaPagamentoEnum.values()) {
        if (method.value == value) {
          return method;
        }
      }
      throw new IllegalArgumentException("Valor inválido: " + value);
    }
  }
  
  private FormaPagamentoEnum formaPagamentoEnum;
  
  public FormaPagamentoFactoryImpl(int formaPagamentoId) {
    this.formaPagamentoEnum = FormaPagamentoFactory.fromValue(formaPagamentoId);
  }
  
  @Override
  public FormaPagamento create() {
     if (this.formaPagamentoEnum == FormaPagamentoEnum.CREDITO) {
       return new PagamentoCredito();
     }
     if (this.formaPagamentoEnum == FormaPagamentoEnum.DEBITO) {
       return new PagamentoDebito();
     }
     return null;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ProcessadorPagamento {

  private FormaPagamentoFactory formaPagamentoFactory;
  
  public ProcessadorPagamento(FormaPagamentoFactory formaPagamentoFactory) { 
    // dependemos de uma abstração
    this.formaPagamentoFactory = formaPagamentoFactory;
  }
  
  public void processarPagamento(Dados dados) {
    FormaPagamento formaPagamento = this.formaPagamentoFactory.create(dados.getFormaPagamento());
    if (formaPagamento == null) {
      throw new RuntimeException("Forma de pagamento não suportada ou desconhecida");
    }
    formaPagamento.processarPagamento(dados); 
    // formaPagamento é abstrato, não precisamos detalhes de sua implementação
  }
}

dip_dependencias_1

Apesar do nosso código aumentar consideravelmente de tamanho, ganhamos em flexibilidade, extensibilidade, desacoplamento e testabilidade. Utilizamos uma Abstract Factory para construir nossas classes concretas, além de utilizarmos o Strategy pattern para definir um comportando em comum (FormaPagamento) e implementar cada comportamento específico, crédito e débito. Observe que FormaPagamentoFactoryImpl quebra dois princípios SOLID: Single responsibility principle (SRP) e Open Close Principle (OCP).

  • SRP = FormaPagamentoFactoryImpl é responsável por criar e mapear um enum para uma instância concreta.
  • OCP = Caso precisássemos criar uma nova foram de pagamento, teríamos que modificar FormaPagamentoFactoryImpl.

Seria possível resolver essas duas quebras com Registry pattern, utilizando um HashMap com as keys (Enums) e values (Classes concretas) no Abstract Factory e instanciar cada uma dinamicamente, ou utilizar alguma outra técnica como Locator Pattern, mas todas elas possuem seus trade-offs e cabe ao desenvolvedor avaliar os requisitos não funcionais, como desempenho, flexibilidade necessária, etc. Lembra-se que SOLID serve de guia e não precisa e nem deve, se não você fica maluco ser seguido a risca em todos os níveis.

Dependency Inversion Principle (DIP) vs Inversion of Control (IoC) vs Dependency Injection (DI)

É muito comum, mesmo para programadores experientes confundirem-se com os termos, pois são íntimos de alguma forma, mas não são o mesmo. DIP se trata de evitar ao máximo referenciar objetos concretos, já IoC é outro princípio que se trata de como e qual momento de criação do objeto, ou seja, invertendo esse controle de criação, sendo responsabilidade de uma entidade externa de criar e injetar dependências. No caso do famoso framework Spring, temos o IoC Container que fica responsável por gerenciar o ciclo de vida dos beans, incluindo construção, injeção de dependencies de classes e armazenado em um IoC Container.

Em resumo:

  • IoC = Principio sobre o momento e como objeto será criado, delegado a uma entidade externa, livrando o desenvolvedor de lidar com essa responsabilidade.
  • DI = É uma implementação do princípio IoC, sendo considerado um padrão que o próprio Spring utiliza (A exemplo do @Autowired).
  • DIP = Principio que nos guia a evitar acoplamento referenciando classes e métodos concretos.

Conclusão

DIP é um princípio poderoso e combinado com Design patterns, podem aumentar a vida util do seu projeto (manutenabilidade), melhorando até mesmo o desempenho do seu time na construção de features a longo prazo, visto que o mesmo provê uma base de código extensível e flexível.

This post is licensed under CC BY 4.0 by the author.