Princípios SOLID – O princípio de inversão de dependência

Por dti digital|
Atualizado: Jul 2023 |
Publicado: Fev 2021

SOLID é um acrônimo mnemônico que relaciona um tópico de boas práticas de programação a cada letra. A aplicação desses princípios tem por objetivo deixar o projeto mais coeso, reaproveitável e torna a sua manutenção mais simples. Nesse post, vamos falar sobre o princípio de inversão de dependência. é sobre esse princípio que falaremos nesse artigo!

O que é o princípio SOLID?

Basicamente, SOLID são cinco princípios da programação orientada a objetos que facilitam no desenvolvimento de softwares, tornando-os fáceis de manter e estender. Esses princípios podem ser aplicados a qualquer linguagem de POO.

A sistemática dos princípios SOLID tornam o software mais robusto, escalável e flexível, deixando-o tolerante a mudanças, facilitando a implementação de novos requisitos para a evolução e manutenção do sistema.

Assista o vídeo para entender melhor:

Quer ver mais conteúdos como esse?

Por que usar SOLID?

Os princípios SOLID devem ser aplicados para se obter os benefícios da orientação a objetos, tais como:

  • Seja fácil de se manter, adaptar e se ajustar às alterações de escopo;
  • Seja testável e de fácil entendimento;
  • Seja extensível para alterações com o menor esforço necessário;
  • Que forneça o máximo de reaproveitamento;
  • Que permaneça o máximo de tempo possível em utilização.

Utilizando os princípios SOLID é possível evitar problemas muito comuns:

  • Dificuldade na testabilidade / criação de testes de unidade;
  • Código macarrônico, sem estrutura ou padrão;
  • Dificuldades de isolar funcionalidades;
  • Duplicação de código, uma alteração precisa ser feita em N pontos;
  • Fragilidade, o código quebra facilmente em vários pontos após alguma mudança.

Quais são os princípios SOLID?

S.O.L.I.D: Os 5 princípios da POO

S — Single Responsiblity Principle (Princípio da responsabilidade única)
O — Open-Closed Principle (Princípio Aberto-Fechado)
L — Liskov Substitution Principle (Princípio da substituição de Liskov)
I — Interface Segregation Principle (Princípio da Segregação da Interface)
D — Dependency Inversion Principle (Princípio da inversão da dependência)

SRP — Single Responsibility Principle:

Princípio da Responsabilidade Única Uma classe deve ter um, e somente um, motivo para mudar.
Esse princípio declara que uma classe deve ser especializada em um único assunto e possuir apenas uma responsabilidade dentro do software, ou seja, a classe deve ter uma única tarefa ou ação para executar.

Classe que não está de acordo com o SRP:

public class ClienteController {
public void save(ClienteVO c)
{
if (this.validate(c))
{
ClienteDAO.getInstance().save(c);
this.sendEmail();
}
}
private boolean validate(ClienteVO c) {
if (c.nome.equals(""))
return true;
}
private void sendEmail()
{
MimeMessage msg = new MimeMessage(session);
msg.setFrom(new InternetAddress("email@email.com"));
...
Transport.send(msg);
}
}

No código acima, tem-se uma fictícia classe ClienteController, onde são criadas algumas funcionalidades relativas a persistência de dados, validação e envio de email.

Apesar do código compilar, a classe ClienteController está desempenhando diversos papéis (responsabilidades) que não são inerentes à ela. Por exemplo, a classe está validando uma informação, persistindo o objeto e enviando e-mail.

Neste contexto, deve-se separar cada funcionalidade em classes, mesmo que a princípio seja gerado acoplamento entre elas. Ou seja, a validação deve ser formada por uma classe cuja única responsabilidade seja validar um campo. Existem diversos tipos de validação, como validar se um campo é nulo ou se um e-mail está escrito corretamente.

Seguindo o princípio, cada tipo de validação deve ser uma classe, já que cada validador é uma responsabilidade única:

Classe que está de acordo com o princípio:

public class Retangulo extends Figura{
public void area()
{
}
public void desenhar()
{
}
}

No contexto de uma classe que representa um retângulo, a responsabilidade deste retângulo é conter ações inerentes a ele, sendo que a responsabilidade de desenhar (draw) não é relativa à classe retângulo, mesmo porque uma responsabilidade de desenho é muito específica em relação ao ambiente em si.

A quebra desta responsabilidade pode significar diversos problemas de implementação. No exemplo anterior, se houvesse a necessidade de desenhar um retângulo em ambientes diferentes, possivelmente as classes teriam que ser refeitas, ou então o uso de condições seria utilizado para distinguir o ambiente em si.

OCP — Open-Closed Principle:

Princípio Aberto-Fechado — Objetos ou entidades devem estar abertos para extensão, mas fechados para modificação, ou seja, quando novos comportamentos e recursos precisam ser adicionados no software, devemos estender e não alterar o código fonte original.

LSP— Liskov Substitution Principle:

Princípio da substituição de Liskov — Uma classe derivada deve ser substituível por sua classe base.
O princípio da substituição de Liskov foi introduzido por Barbara Liskov em sua conferência “Data abstraction” em 1987. A definição formal de Liskov diz que:

Se para cada objeto o1 do tipo S há um objeto o2 do tipo T de forma que, para todos os programas P definidos em termos de T, o comportamento de P é inalterado quando o1 é substituído por o2 então S é um subtipo de T

Um exemplo mais simples e de fácil compreensão dessa definição. Seria:

se S é um subtipo de T, então os objetos do tipo T, em um programa, podem ser substituídos pelos objetos de tipo S sem que seja necessário alterar as propriedades deste programa. — Wikipedia.

Exemplos de violação do LSP:
Sobrescrever/implementar um método que não faz nada;
Lançar uma exceção inesperada;
Retornar valores de tipos diferentes da classe base;

ISP — Interface Segregation Principle:

Princípio da Segregação da Interface — Uma classe não deve ser forçada a implementar interfaces e métodos que não irão utilizar. Esse princípio basicamente diz que é melhor criar interfaces mais específicas ao invés de termos uma única interface genérica.

DIP — Dependency Inversion Principle:

Princípio da Inversão de Dependência — Dependa de abstrações e não de implementações.
De acordo com Uncle Bob, esse princípio pode ser definido da seguinte forma:

1. Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender da abstração.
2. Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.

Inicialmente pode parecer um pouco estranho pensar na palavra “inversão” presente na nomeação desse conceito. Para entender isso, é importante entender como funcionava o desenvolvimento de software antes da formação desse conceito.

Métodos mais tradicionais de software utilizam uma abordagem em que módulos de alto nível dependem de módulos de baixo nível, ou seja, a política de alto nível aplicada à um objeto de baixo nível depende diretamente de como é esse objeto de baixo nível. Vamos ver um exemplo simples de aplicação desse conceito: imagine um botão e uma lâmpada.

Utilizando um diagrama para ilustrar como um modelo seria implementado sem o principio de inversão de dependência, o modelo ficaria assim:

Em que o botão ligaria a lâmpada, caso ela esteja desligada, e desligaria a lâmpada, caso ela esteja ligada.

E por que esse programa não seria adequado?

Simples, o seu programa Button somente seria adequado para ligar ou desligar uma lâmpada. Caso o dono do sistema deseje que esse botão seja utilizado para desligar um portão eletrônico, por exemplo, deve ser feitas grandes mudanças no método do Poll do Button para que o botão passe a funcionar onde ele é necessário. Ou seja, um modulo de alto nível (Button) está dependendo de um módulo de baixo nível (Lamp).

Como é a implementação utilizando o princípio de inversão de dependência?

Para isso nos deparamos com outro conceito importante quando o assunto é o princípio da inversão de dependência: a divisão em camadas. Para que seja possível e fique clara a aplicação do princípio de inversão de dependência é necessário que mais um camada seja adicionada. Essa camada será responsável pela intermediação do botão com o modulo da lâmpada, deixando o modelo do sistema da seguinte forma:

braulio3

Com isso, o Button fará uma implementação que vai utilizar o turnOn e turnOff oferecidos pelo ButtonServer. Enquanto isso, a Lamp detalhará ao ButtonServer quais os detalhes necessários para que o turnOn e turnOff sejam executados corretamente.

O ButtonServer utiliza o conceito de abstração. Ou seja, as funcionalidades turnOn e turnOff apresentadas por ele serão apenas representativas, abstratas, e somente serão implementadas quando um módulo de baixo nível, ao qual essas funcionalidades são aplicadas, apresentar os detalhes necessários para a execução.

Dessa forma, voltando ao nosso exemplo, caso seja necessário que o botão também seja utilizado para a abertura de um portão eletrônico, somente será necessário adicionar ao modelo um motor. Este representa os detalhes necessários para que as funções turnOn e turnOff direcionadas ao motor do portão sejam executadas. O modelo então fica assim:

braulio4

Conclusão

Por fim, pode-se observar um isolamento entre a camada de negócios dos detalhes. O comportamento do botão independe completamente de qual objeto ele está ligando ou desligando. Sua lógica de ligar quando está desligado e desligar quando está ligado estará sempre intocada e será aplicada em qualquer um dos casos.

 

Quer saber mais?