SOLID na Prática! (Parte 1: Single Responsibility)
SOLID é uma daquelas coisas que TODO DEV deveria tatuar no braço! Acompanhando de outros conceitos, ele é a base para o desenvolvimento de um código “minimamente bom”.
SOLID é um acrônimo define cinco princípios para programação orientada a objetos, cinco “práticas”, cinco “postulados de design”. Esses, com objetivo de garantir que seu código não seja um belo macarrão. Que ele seja evolutivo e que a manutenção do código, assim como implementação de novas features, sejam fáceis.
O conteúdo inteiro será abordado em 5 artigos, um para cada “letra”, um para cada princípio. Dessa forma conseguimos explorar o “como aplicar os princípios na prática” e acabar de vez com todas as dúvidas!
Abaixo uma rápida apresentação do significado do acrônimo SOLID:
- S — Single Responsibility Principle (Princípio de única responsabilidade)
- O —Open-Closed Principle (Princípio de Aberto/Fechado)
- L— Liskov Substitution Principle (Princípio da substituição de Liskov)
- I — Interface Segregation Principle (Princípio da segregação de Interface)
- D — Dependency Inversion Principle (Princípio da inversão de dependência)
Single Responsiblity Principle
“Uma classe deve ter apenas um motivo para mudar, deve ter apenas uma única responsabilidade.”
Esse conceito pode (e deve) ser extendido para outros níveis de escopo, como métodos, por exemplo. Violar essas regras dificulta absurdamente a manutenção do código. Uma alteração de em um contexto com “multiresponsabilidade”, que deveria afetar somente uma, vai causar mudança em outros contextos. É criado um alto acoplamento, mais dependência entre os elementos, o reaproveitamento do código se torna baixo e com alta incidência de código duplicado. Tudo isso contribui com o pior cenário do mundo: Um código sem testes ou com testes ruins. Essa complexidade vai te impedir de ter testes efeciêntes e recursos como “mock” (objetos simulados) para efetivamente testar uma unidade sem suas dependências.
Para facilitar, falaremos da menor unidade para a maior. No exemplo abaixo temos um método claramente com acúmulo de responsabilidades:
Conseguimos visualizar que o método acima está, de forma simbólica, processando um pagamento em uma instituição de pagamento, enviando email para o cliente e salvando essas informações no banco de dados.
Mesmo sendo um serviço “orquestrado” (pensando em um “service”) ele não teria ter tanta responsabilidade inclusive na implementação do “como salvo uma informação no banco”, “como me conecto e envio email para um servidor específico” etc.
No primeiro momento, independente de qual método essa responsabilidade ficará, precisamos “extrair” essas responsabilidades para outros lugares (métodos). Preciso que este método SOMENTE orquestre a criação do pagamento. Em outro, SOMENTE o envio de notificação. E por último, mais um método que SOMENTE faça persistência dos dados no banco.
No exemplo acima, os métodos assumem responsabilidades únicas. Mas, a classe ainda está com muitas responsabilidades, afinal, o contexto de “enviar email” e “salvar no banco” continuaram na classe, que deveria somente orquestrar fluxos de pagamento (como processamento de um pagamento, estorno de um pagamento etc). Vamos pensar em um nível maior agora, na classe. Como deixamos sua responsabilidade única e bem definida no código?
No código acima, separamos as classes por responsabilidade. Além disso, foram incluídos outros conceitos que veremos em detalhes mais a frente, como uso de interfaces, injeção de dependência etc.
O código (para mim e acredito que para a maioria das pessoas) fica nitidamente mais legível, mais organizado e de fácil manutenção. Uma alteração em uma parte, afeta “somente” essa parte. Se você tem testes unitários, você consegue testar “apenas” um ponto do sistema, um comportamento específico. E quando você precisasse testar o “PaymentService”, faria o uso de algum “mock” para simular o comportamento do “INotificationService”.
Ou seja, por mais que meu serviço orquestrador esteja usufruindo de outros recursos (email, banco de dados), não é necessário utilizar um servidor de email ou banco de dados “real” para testar o fluxo. Tudo pode ser simulado. Obviamente esses componentes podem ser testados separadamente.
Contextos (responsabilidades) separados, correções e alterações feitas isoladamente, testes testando apenas um pedaço do seu código sem interferência de “unidades” externas… Hum.. estamos começando a falando sobre baixo acoplamento no seu código! Parabéns! Vamos ganhar até um biscoitinho! hehehe
Além da implementação em si, uma boa prática para reduzir as falhas subjetivas de percepção e entendimento do problema e da solução proposta, é a limitação do tamanho máximo do escopo. No meu dia-a-dia gosto de trabalhar com métodos limitados em 25 linhas e, para classes, um máximo de 300 linhas. Para garantir que isso está sendo seguido, utilizo ferramentas de análise de código e qualidade (como Sonarqube, Codacy, CodeClimate etc), que são capazes de fazer essa validação automaticamente e impedir, por exemplo, que o novo código (pull request) siga adiante quando em desobediência da regra.
Não se esqueça, JAMAIS! O Clean Code é uma prática que facilita a aplicação do SOLID (e vice-versa). Se você quer saber um pouco mais de Clean Code, da uma olhada nesse link, que lá explico um pouco mais sobre o assunto, também com alguns exemplos práticos.
https://medium.com/thiagobarradas/clean-code-por-um-mundo-com-código-melhores-89f6b3699ace
No próximo artigo desta sequência, falaremos sobre Open-Closed Principle (Princípio do Aberto/Fechado) também com exemplos práticos e um tanto da minha análise pessoal por cima do assunto! Até a próxima! 👋