Princípios SOLIDES: O Guia Definitivo

Princípios SOLIDOS

SOLID é um acrônimo que representa cinco princípios muito importantes quando desenvolvemos com o paradigma OOP, além disso, é um conhecimento essencial que todos os desenvolvedores devem saber.
Compreender e aplicar esses princípios permitirá que você escreva código de melhor qualidade e, portanto, seja um desenvolvedor melhor.

Os princípios SOLID foram definidos no início dos anos 2000 por Robert C. Martin (tio Bob) . O tio Bob elaborou alguns desses e identificou outros já existentes e disse que esses princípios deveriam ser usados ​​para obter um bom gerenciamento de dependências em nosso código.

No entanto, no início, esses princípios ainda não eram conhecidos como SOLID até que Michael Feathers observasse que as iniciais desses princípios se encaixavam perfeitamente sob o acrônimo SOLID e que também era um nome muito representativo para sua definição.

Esses princípios são um conjunto de recomendações práticas que, quando aplicado ao nosso código, nos ajuda a obter os seguintes benefícios:

  • Facilidade de manutenção.
  • Facilidade para estender.
  • Código robusto.

Mas antes de ver o que significa cada princípio SÓLIDO, precisamos lembrar dois conceitos relevantes no desenvolvimento de qualquer software.
O acoplamento e a coesão :

Acoplamento:

Podemos defini-lo como o grau em que uma classe, método ou qualquer outra entidade de software está diretamente vinculado a outro . Este grau de acoplamento também pode ser visto como um grau de dependência.

  • exemplo: quando queremos usar uma classe fortemente vinculada (com um alto acoplamento) para uma ou mais classes, acabaremos usando ou modificando partes dessas classes para as quais somos dependentes.

Coesão:

A coesão é a medida em que duas ou mais partes de um sistema funcionam juntas para obter melhores resultados do que cada parte individualmente .

  • exemplo: Han Solo e Chewbacca a bordo do Millennium Falcon.

Para obter um bom software, devemos sempre tentar ter um baixo acoplamento e uma alta coesão , e os princípios SOLID nos ajudam com essa tarefa. Se seguimos estas diretrizes, nosso código será mais robusto, sustentável, reutilizável e extensível e evitaremos a tendência do nosso código para quebrar em muitos lugares sempre que algo for alterado.

Vamos dividir as letras do SOLID e ver os detalhes de cada um desses.

Princípio de Responsabilidade Única (SRP):

Uma classe deve ter apenas uma razão para mudar.

Este princípio significa que uma classe deve ter apenas uma responsabilidade e fazer apenas a tarefa para a qual foi projetada .

Caso contrário, se nossa classe assumir mais de uma responsabilidade, teremos um alto acoplamento, fazendo com que nosso código seja frágil com quaisquer alterações.

Benefícios:

  • Acoplamento reduzido.
  • Código mais fácil de entender e manter.

Violação do SRP:

  • Temos uma classe de Clientes com mais de uma responsabilidade:

storeCustomer (String name) tem a responsabilidade de armazenar um Cliente no banco de dados, por isso é uma responsabilidade de persistência e deve estar fora da classe do Cliente.

generateCustomerReport (String name) tem a responsabilidade de gerar um relatório sobre o Cliente, assim também deve estar fora da classe do Cliente

Quando uma classe tem múltiplas responsabilidades, é mais difícil entender, ampliar e modificar.

Melhor solução:

Criamos diferentes classes para cada responsabilidade .

  • Classe de cliente :
  • Classe CustomerDB para a responsabilidade de persistência:
  • Classe CustomerReportGenerator para a responsabilidade da geração de relatórios:

Com esta solução, temos algumas aulas, mas cada classe com uma única responsabilidade, de modo que obtemos um baixo acoplamento e uma alta coesão.

Princípio aberto fechado (OCP):

As entidades de software (classes, módulos, funções, etc.) devem estar abertas para extensão, mas fechadas para modificação

De acordo com este princípio, uma entidade de software deve ser facilmente extensível com novos recursos sem ter que modificar seu código existente em uso.

aberto para extensão: novos comportamentos podem ser adicionados para satisfazer os novos requisitos.

fechado para modificação: não é necessário ampliar o novo comportamento modificar o código existente.

Se aplicarmos este princípio, obteremos sistemas extensíveis que serão menos propensos a erros sempre que os requisitos forem alterados. Podemos usar a abstração e o polimorfismo para nos ajudar a aplicar este princípio.

Benefícios:

  • Código sustentável e reutilizável.
  • Código mais robusto.

Violação de OCP:

  • Temos uma classe Rectangle :
  • Além disso, temos uma classe Square :
  • E temos uma classe ShapePrinter que desenha vários tipos de formas:

Podemos ver que sempre que quisermos desenhar uma forma distinta, teremos que modificar o método drawShape do ShapePrinter para aceitar uma nova forma .

À medida que novos tipos de formas chegam a desenhar, a classe ShapePrinter será mais confusa e frágil para as mudanças.

Portanto, a classe ShapePrinter não está fechada para modificação .

Uma solução:

  • Nós adicionamos uma classe Abstrato de Forma :
  • Refletor Rectangle classe para estende de Forma :

Refactor Square class para estender de Shape :

  • Refactor de ShapePrinter:

Agora, a classe ShapePrinter permanece intacta quando adicionamos um novo tipo de forma.
O código existente não é modificado .

Então, se quisermos adicionar mais tipos de formas, precisamos criar uma classe para essa forma.

Outra solução:

Agora, com esta solução, a classe ShapePrinter também permanece intacta quando adicionamos um novo tipo de forma porque o método drawShape recebe abstrações de Forma .

  • Nós mudamos Shape para uma interface:
  • Classe Rectangle Refactor para implementar Shape :
  • Classe Refactor Square para implementar Shape :
  • ShapePrinter :

Liskov Princípio da Substituição (LSP):

Os objetos em um programa devem ser substituíveis com instâncias de seus subtipos sem alterar a correção desse programa.

Este princípio foi definido por Barbara Liskov e diz que os objetos devem ser substituídos por instâncias de seus subtipos sem alterar o funcionamento correto do nosso sistema.

Aplicando este princípio, podemos validar que nossas abstrações estão corretas.

Benefícios:

  • Código mais reutilizável.
  • As hierarquias de classe são fáceis de entender.

O exemplo clássico que geralmente explica esse princípio é o exemplo Retângulo.

Violação do LSP:

  • Temos uma classe Rectangle :
  • E uma classe quadrada :

Uma vez que um quadrado é um retângulo (matematicamente falando), decidimos que o quadrado seja uma subclasse de retângulo .

Realizamos a substituição de setHeight () e setWidth () para definir as duas dimensões (largura e altura) para o mesmo valor para que as instâncias do Square permaneçam válidas.

Então, agora poderíamos passar uma instância Square onde uma instância de Retângulo é esperada.

Mas se fizermos isso, podemos quebrar os pressupostos sobre o comportamento do Rectangle:

O próximo pressuposto é verdadeiro para Rectangle:

Mas a mesma suposição não é válida para o Square:

O quadrado não é uma substituição correta para o retângulo, pois não cumpre o comportamento de um retângulo .

A hierarquia quadrada / retângulo em isolamento não mostrou nenhum problema no entanto, isso viola o Princípio de Substituição de Liskov!

Uma solução:

  • Usando uma interface Shape para obter a área:
  • Refatoração de Rectangle para implementar Shape:

Refactoring do Square para implementar Shape:

Outra solução que é frequentemente aplicada (com imutabilidade ):

  • Refatoração de retângulo:
  • Refatoração do quadrado para estender o retângulo:

Muitas vezes modelamos nossas aulas de acordo com as propriedades do objeto do mundo real que queremos representar. Mas é mais importante que prestem atenção aos comportamentos para evitar esse tipo de erros.

Princípio de segregação da interface (ISP):

muitas interfaces específicas do cliente são melhores do que uma interface de propósito geral

Este princípio define que uma classe nunca deve implementar uma interface que não vá usar . O não cumprimento deste princípio significa que, em nossas implementações, teremos dependências de métodos que não precisamos, mas que somos obrigados a definir.

Portanto, implementar interfaces específicas é melhor implementar uma interface de propósito geral . Uma interface é definida pelo cliente que irá usá-lo, portanto, não deve ter métodos que este cliente não implementará.

Benefícios:

  • Sistema desacoplado.
  • Código fácil de refatorar.

Violação do ISP:

  • Temos uma interface de carro :
  • E uma classe Mustang que implementa o carro :

Agora, temos um novo requisito para incorporar um novo modelo de carro:
Um DeloRean , mas não é um DeLorean comum. Nosso DeloRean é muito especial e tem a característica de viajar no tempo.

Como de costume, não temos tempo para fazer uma boa implementação e, além disso, o DeloRean deve retornar ao passado com urgência.
Então decidimos:

  • Adicione dois novos métodos para o nosso DeloRean na interface do carro :
  • Agora, nossa classe DeloRean implementa o carro :
  • Mas agora a classe Mustang é forçada a implementar os novos métodos para cumprir a interface do carro :

Neste caso, o Mustang viola o Princípio de Segregação de Interface porque deve implementar métodos que não usam.

Uma solução com segregação de interfaces:

  • Interface do Refactor Car :
  • Adicione uma interface TimeMachine :
  • Refactor Mustang (implementa apenas a interface do carro) :
  • Refactor DeloRean (implementa Car e TimeMachine) :

Princípio de inversão de dependência (DIP):

Os módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.

As abstrações não devem depender dos detalhes. Os detalhes devem depender das abstrações.

O princípio de inversão de dependência significa que uma classe específica não deve depender diretamente de outra classe, mas em uma abstração (interface) dessa classe.

Quando aplicamos esse princípio, reduziremos a dependência de implementações específicas e, assim, tornaremos nosso código mais reutilizável.

Benefícios:

  • Reduza o acoplamento.
  • Código mais reutilizável.

Violação do DIP:

  • Temos uma classe DeliveryDriver que representa um driver que funciona para uma empresa de entrega:
  • DeliveryCompany que administra envios:

Observe que DeliveryCompany cria e usa concreções DeliveryDriver. Portanto, DeliveryCompany, que é uma classe de alto nível, depende de uma classe de nível inferior e isso é uma violação do Princípio de Inversão de Dependência .

Uma solução:

  • Criamos a interface DeliveryService para ter uma abstração:
  • Classe DeliveryDriver do Refactor para implementar DeliveryService :
  • Entrega de Refactor Empresa que agora depende de uma abstração e não de uma concreção:

Agora, as dependências são criadas em outro lugar e são injetadas através do construtor da classe.

É importante não confundir este princípio com a Injeção de Dependência que é um padrão que nos ajuda a aplicar este princípio para garantir que a colaboração entre as classes não envolva dependências entre eles.

Existem várias bibliotecas que facilitam a injeção de dependência, como Guice ou Dagger2, que é uma das mais populares.

Conclusão

Seguir os princípios SOLID é essencial se formos a construir um software de qualidade que seja fácil de estender, robusto e reutilizável. Também é importante não esquecer de ser pragmático e usar o senso comum, porque às vezes a sobre-engenharia pode tornar as coisas simples mais complexas.

Obrigado pela leitura.
E se você gostou, clique no ícone ❤ abaixo.

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *