top of page

Princípios SOLID

Introdução

"S.O.L.I.D Design Principles" algo bastante comentado por desenvolvedores, principalmente por aqueles com alguma senioridade a mais, que vieram de tecnologias como Java ou C#, mas sempre algo na nossa bolha de programação orientada a objetos.

No post de hoje, abordaremos cada um dos princípios que compõem essa sigla e entenderemos o porque e como usar.


S. Single Responsibility Principle

Também chamado de "SRP" o "Princípio de Responsabilidade Única" tem um nome bastante explicativo. Ele diz que cada parte do seu código deve ser responsável por fazer uma única coisa, e fazer ela bem feita. Esse princípio remete muito a filosofia Unix, quando ela diz:

Faça com que um programa faça uma coisa bem feita. Para desempenhar uma nova função, comece algo novo ao invés de complicar programas antigos com a adição de novas "funcionalidades".

Esse princípio é na minha opinião um dos, se não o mais fácil de violar. É muito comum que vejamos uma classe e decidamos colocar mais uma coisinha nela ao invés de criar uma classe nova, usando desculpas como "Ah, mas é só uma inserção no banco de dados" ou "É só um log, não é nada demais" e após isso ficamos com uma classe que busca contatos, insere novos no banco de dados, envia um e-mail e ainda salva um log dizendo o que foi feito, quando e como. A pior parte disso é que não só a manutenção fica cada vez mais complexa, como o código vai perdendo o sentido e ficando demasiadamente acoplado.


Seguindo nessa mesma linha, vamos a um exemplo de como seria um código incorreto:



Essa classe "ContactService" gerencia contatos, que é o esperado pelo nome dela, porém tem também a responsabilidade de enviar um email, o que não é algo esperado. Ainda mais em uma situação de desenvolvimento ágil, é muito comum que não tenhamos documentação de código, embora essa seja bem importante.


A correção dessa classe seria a separação em duas classes diferentes. A primeira que tem responsabilidade sobre gerenciamento de contatos:



E a segunda que faz o envio de emails, e que pode ser estendida para outras funções relacionadas.




O. Open-Closed Principle (OCP)

É o princípio "aberto-fechado" que diz que uma classe/função pode ser estendida (aberta) porém não modifica o código existente (fechada). Esse princípio garante flexibilidade e adaptabilidade para atender novas necessidades, sem comprometer a estabilidade do código já existente.


Para essa situação, nossa base é a criação de relatórios em diversos formatos.


Vamos aos exemplos:



O código acima está errado, porque ele exige alteração do código para extensão do mesmo, o que pode trazer erros e problemas inesperados em partes do código que dependam dessa classe. A forma correta seria a seguinte:



Nesse exemplo, temos uma interface, que define um contrato de formatação e classes concretas de String e XML que implementam esse contrato. A classe "ReportFormatter" recebe um objeto "ReportFormat" no construtor, que permite a injeção de diferentes implementações de formatação.


Utilizando essa abordagem, é possível adicionar novos formatos de relatório criando novas classes que implementam "ReportFormat" sem modificar o código que já existe, evitando assim efeitos colaterais.


L. Liskov Substitution Principle (LSP)

O Princípio da Substituição de Lizkov é um ponto que eu considero particularmente complexo. Ele permeia o design e arquitetura do projeto em todos os níveis e sempre deve ser levado em conta.


Ele afirma que a substituição de um pai por seu herdeiro sempre deve ser possível, sem causar comportamentos inesperados ou erros. Em outras palavras, se nós temos uma classe "Animal" com um método "comer()", e uma classe "Cachorro" que herda de "Animal", e implementa o método "comer()", então deve ser possível usar um "Cachorro" em qualquer situação que seria esperado utilizar um "Animal".


Antes de explicar com Apex, uma implementação bastante simplista do que foi dito acima em Python, apenas para ilustração do princípio:




Lembrando que o LSP não exige que o herdeiro possa substituir o pai em todos os contextos. Por exemplo, se o cachorro late e o gato mia, sem que o Animal implemente esses métodos, não seria possível utilizar um gato ou um cachorro em seu lugar.


Em resumo:

  • O LSP se concentra na substituição do pai pelo herdeiro.

  • O herdeiro não precisa ser capaz de substituir o pai em todos os contextos.

  • O importante é que o herdeiro possa ser usado em qualquer contexto que o pai seja esperado, sem causar erros ou comportamentos inesperados.


Agora para os exemplos em apex, que são nosso foco aqui:


Imagine um cenário onde temos uma classe "TicketCliente" para lidar com tickets de suporte ao cliente. Existe também uma classe derivada chamada "TicketEscalonado" para casos escalados que requerem tratamento especial.



Esse código viola o LSP porque um "ticketEscalonado" substituindo um "TicketCliente" lança uma exceção (pior ainda, uma exceção sem tratamento).


A implementação correta seria:



Aqui temos o "TicketCliente" como uma classe abstrata, definindo o contrato para fechamento de ticket com o método "fecharTicket()". Classes concretas, como "TicketPadrao" e "TicketEscalonado" a sobrescrevem com o comportamento apropriado para casos escalados.


Lembrando os pontos chave:

  • As subclasses devem extender ou cumprir o contrato da classe base sem quebrar a funcionalidade existente.

  • Classes abstratas ou interfaces podem ser usadas para definir contratos para as subclasses.

  • As subclasses podem ter funcionalidades adicionais, mas não devem alterar o comportamento principal de maneira inesperada.


I. Interface Segregation Principle (ISP)

O Princípio da Segregação de Interfaces diz que as interfaces criadas para definir contratos devem ser especificas, a fim de evitar a necessidade de classes implementarem métodos que não são necessários para si. Seguindo esse princípio, o código se torna menos acoplado, o que mais uma vez, facilita a manutenção e extensão do código, promovendo uma implementação que atenda apenas o escopo que é necessário.


Seguindo o ISP, evitamos interfaces "monolíticas", que reúnem funcionalidades diversas e desconexas, que dificultam ou impossibilitam a reutilização e entendimento do código.


Considerando a seguinte interface "Animal":




Todos os animais comem e dormem, o que estaria correto, porém nem todos nadam, o que faria com que nossa interface definisse um contrato com uma função desnecessária.


A forma correta de definir essas interfaces seria a seguinte:



Apex permite que uma classe implemente múltiplas interfaces, então uma implementação desse código seria:



Mas lembre-se, ao iniciar a segregação de interfaces, não exagere, ou você pode criar interfaces extremamente granulares, que são tão difíceis de se utilizar quanto interfaces monolíticas.


D. Dependency Inversion Principle (DIP)

Para simplificar o desacoplamento entre as classes, o Princípio da Inversão de Dependência promove que as classes dependam de abstrações (interfaces) e não de suas implementações concretas.


Vamos aos exemplos e suas explicações:


Errado:



O erro na implementação dessas classes é que a "ContactController" depende diretamente de "ContactService". O que acopla o código e diminui sua testabilidade, uma vez que a Controller precisaria ser modificada para testar diferentes cenários de implementação da Service.


O correto seria o seguinte:



Neste exemplo, a classe "ContactServiceImpl" implementa a interface "ContactService". A classe "ContactController" depende da interface "ContactService" e não da implementação concreta "ContactServiceImpl". Essa inversão de dependência facilita o desacoplamento e a testabilidade do código.

Ao utilizarmos o DIP, permitimos que iterações de desenvolvimento sejam feitas estendendo nossas aplicações. Esse princípio nos leva a uma ideia de desenvolvimento com comunicação por meio de interfaces, que permite as iterações acima citadas acolhendo mudanças e inovações, com menores riscos de quebrar o sistema.

É possível dizer que:

Uma Classe A não deve conhecer nenhum detalhe de implementação da Classe B. Uma interface deve ser utilizada para fins de comunicação.

Também podemos lembrar que podemos em conjunto aplicar outros princípios vistos acima, como a criação de novas classes filhas herdando de uma interface X, para não violarmos o LSP. Ou a implementação correta das interfaces "atômicas" que garante conformidade com o ISP.


Casos de uso

Assim como Design Patterns, e modelos de desenvolvimento específicos, como Test Driven Development ou Domain Driven Design, existem diversas situações onde a aplicação dos princípios S.O.L.I.D. são de grande valor, mas também existem situações onde sua implementação pode não ser a mais ideal. Como os casos de aplicação são muito diversos (e na minha opinião sempre recomendados, visto que permitem o crescimento saudável da codebase) segue aqui alguns exemplos de casos de uso:

  • Criação de classes reutilizáveis e extensíveis

  • Desenvolvimento de código modular e testável

  • Redução de acoplamento entre classes

  • Melhoria da legibilidade e manutenabilidade do código

  • Aumento da confiabilidade e robustez das aplicações

Você pode perceber que esses não são casos de uso, mas sim, características do sistema, e isso é proposital. É muito complexo apontar o dedo para uma situação e dizer que ela não se beneficiaria da aplicação desses princípios de desenvolvimento. O que é mais viável é entender se essas características lhe são importantes. Caso a resposta seja sim, pode ser interessante adotar esse modelo.

É importante também ressaltar a necessidade de que a equipe esteja de acordo em seguir essas práticas, porque caso apenas uma pequena parcela das pessoas utilize, eventualmente as interfaces criadas para diminuir a complexidade apenas serão abandonadas e utilizadas mais em partes legado do sistema. Ressalto também que não é necessário começar implementando tudo o que foi visto aqui. Em uma situação de adições em uma codebase que não segue esses princípios, começar a adotar suas definições aos poucos já é um grande primeiro passo.


Conclusão

Os princípios são padrões já testados e conhecidos de como diminuir a complexidade do desenvolvimento de um sistema complexo enquanto ao mesmo tempo melhora sua escalabilidade, testabilidade, manutenabilidade e robustez. Seguir estes princípios leva a criação de aplicações mais eficientes e confiáveis para atender os requisitos que levaram a criação desse sistema.

Hoje, iniciamos uma possível série, caso seja interessante para você que está lendo esse post, sobre desenvolvimento orientado a objetos, com foco em Salesforce, que nos levará por boas práticas, o que é orientação a objetos, injeção de dependência e meu tópico preferido atualmente, Design Patterns. Caso ache isso interessante, me manda um oi no LinkedIn com uma sugestão de qual deveria ser o próximo tema :)


Fontes

 
 
 

コメント


© 2023, BrazucaForce - Todos os direitos reservados

Políticas de Privacidade

bottom of page