SOLID Principles: melhorando o design do seu código

21/7/2020
Luis Fernando T. Silva
Luis Fernando T. Silva
Software Engineer

Formado em Ciência da Computação pela Unesp, sou um Engenheiro de Software apaixonado por novas tecnologias, viciado em vídeo e cappuccinos.

Está sem tempo para ler? Aperte o play para escutar o artigo.

Antes de falar sobre SOLID em si, gostaria que o leitor fizesse uma reflexão sobre o quão rápido a nossa área sofre atualizações. 

Em um determinado momento existe um grande hype do mercado para contratar devs especialistas em Ruby, depois de um tempo a moda passa a ser um novo framework Javascript revolucionário e, quando você menos espera, é surpreendido novamente com o crescente interesse em Flutter.

Tentar aprender todas essas ferramentas que estão na moda de uma vez, com toda certeza, não é o caminho certo para se aperfeiçoar na carreira.

“Então devo desistir de estudar?”

A resposta é não. Devemos pensar que todo conhecimento da nossa área pode ser organizado como se fosse uma cebola, na qual as camadas mais externas ficam os frameworks e as linguagens de programação e conforme vamos adentrando, encontramos conhecimentos que são mais difíceis de mudar, por exemplo, design patterns e design principles

Conhecer essas camadas mais internas vai te dar poder para não se tornar refém de um framework e/ou linguagem da moda, além de uma maior maturidade para diminuir a barreira de aprendizado de outras tecnologias.

Por onde começar?

Existem vários design principles como KISS, YAGNI, DRY e etc. Porém, um dos que eu mais gosto é o SOLID, um acrônimo para 5 postulados criados por Robert C. Martin (Uncle Bob) escritor do famoso Clean Code.

  • [S]ingle Responsibility Principle (Princípio da responsabilidade Única)
  • [O]pen/Closed Principle (Princípio aberto/fechado)
  • [L]iskov Substitution Principle (Princípio substituição de Liskov)
  • [I]nterface Segregation Principle (Princípio segregação de interfaces)
  • [D]ependecy Inversion Principle (Princípio da inversão de dependências)

Princípio da responsabilidade Única (SRP)

Este princípio prega que uma classe deve ter apenas um motivo para mudar. Em outras palavras, Uncle Bob está falando sobre coesão. Você pode entender coesão como sendo o quão forte é a relação entre os elementos de uma classe, se essa relação é forte, os elementos têm uma alta afinidade entre si. Quanto mais bem definida uma classe é, maior a sua coesão. Quando mais coesa, menos motivos terá para mudar. Ficou difícil? Então vamos para o código.

  • Obs: O conceito de coesão é bem diferente de acoplamento. Acoplamento é quando você tem uma forte dependência entre os elementos, assim alterar um desses vai acarretar mudanças nos outros também.

Exemplo de violação do SRP

<p> CODE:https://gist.github.com/luissilvazup/3d563c1b180f100861b3cdc6d12140cb.js </p>

No código acima temos uma classe com baixa coesão. Qual a relação entre o método login com a classe Employee? Essas duas entidades de software tem uma baixa coesão entre si. Se precisarmos alterar o modo como é realizado o login, vamos ter que modificar a classe Employee. Outra evidência de baixa coesão é o atributo URL, que não tem relação nenhuma com a entidade Employee. Se trocarmos a URL de conexão com o banco, também vamos ter que modificar a classe Employee.

Solução com SRP

Uma solução simples para o problema é criar uma classe para realizar a conexão com o banco, dois models (um para o empregado e outro para o usuário) e por fim, separamos o método de login em uma outra classe.

<p> CODE:https://gist.github.com/luissilvazup/e8223118d9a0dad5d4295bd3a89bae72.js</p>

No exemplo acima, foi criada uma classe para apenas gerenciar a conexão com o banco. Assim, ela passa a ter apenas uma responsabilidade. Abrir uma conexão com o banco.

<p> CODE:https://gist.github.com/luissilvazup/52deb80714d4da3735598fe8163f561a.js </p>

A classe Employee agora ficou bem mais limpa, pois não tem mais interação com o banco. Foi criado também uma classe User para abstrair os campos email e password.

<p> CODE:https://gist.github.com/luissilvazup/987b255c2a14cc87c5060f6d0d6e24fe.js </p>

Por fim, temos a classe AuthenticateLogin que recebe um User como parâmetro do método login. A única responsabilidade desse método é realizar o login do usuário com base nas informações passadas.

Com essa separação de responsabilidades, nossas classes agora só tem um motivo para ser alteradas e seus elementos têm alta coesão entre si. 


Princípio aberto/fechado (OCP)

“Entidades de software (classes, módulos, funções e etc) devem estar abertas para extensão, porém fechadas para modificações.”

O que Uncle Bob quer dizer com essa frase? Devo passar a usar herança toda vez que eu precisar modificar uma classe? 

A resposta é não. 

Uncle Bob não está dizendo para você passar a usar herança (extends) — na verdade, ele está se referindo ao conceito abstrato de extensão. Você pode criar extensões de classes através de herança, classes abstratas e interfaces, por exemplo. 

Exemplo de violação do OCP

Para elucidar, vamos utilizar o método login da classe AuthenticateLogin visto anteriormente. Porém, além do User, ele vai receber um provider que será responsável por decidir qual método de autenticação vamos  utilizar.

<p> CODE: https://gist.github.com/luissilvazup/c8b56b9cd1611f44d1b59044090dc6b0.js </p>

Ao primeiro olhar tudo parece estar em ordem, certo? Não. O que aconteceria se no futuro tivessemos que realizar a autenticação do login usando outras redes sociais? Google, Instagram e Facebook? Nossa classe AuthenticateLogin iria passar a ter um emaranhado de else - if, sendo um para cada rede social. Porém, podemos resolver esse problema de outra forma, fechando nossa classe para modificações, pois do jeito que está no exemplo, ela está completamente aberta para modificações.

Solução com OCP

<p> CODE: https://gist.github.com/luissilvazup/8734c0f9ea445c97de0d3400042286d0.js </p>

No exemplo acima, foi criada a interface Authentication que tem apenas o método abstrato login, assim podemos deixar a estratégia de implementação para classes especializadas que implementam essa interface. 

<p> CODE: https://gist.github.com/luissilvazup/db15deacc4e9da4b8e71c6319f0bae4b.js </p>

A classe AuthenticateLogin apenas tem uma referência para a interface Authentication, assim deixamos para quem for utilizar a classe AuthenticateLogin escolher qual a estratégia de login a ser utilizada, passando via construtor a implementação da interface de sua escolha. Com isso fechamos nossas classes para modificações.


Princípio da substituição de Liskov (LSP)

A terceira letra do acrônimo SOLID, prega que “uma classe derivada deve ser substituível por sua classe base.” 

Pare e a leia novamente. 

Com essa frase, Uncle Bob simplificou a definição científica que Barbara Liskov introduziu em um conferência de 1987. (Sim, é um conceito bem antigo. Lembra das camadas mais internas da cebola que comentei antes?)

Para clarear um pouco, vamos analisar o snippet abaixo.

<p>CODE: https://gist.github.com/luissilvazup/43c3e70d8eaf3b1cffccb20e3acfa011.js </p>

No exemplo temos 2 classes principais, sendo a primeira a BasicAccount com o método yield responsável por render 15% do valor da conta, já a classe SalaryAccount é uma extensão da conta básica, porém não tem a função de render, sendo assim ela lança uma exception caso esse método seja chamado.

No main recuperamos todas as contas salvas no banco e chamamos o método yield, e caso uma SalaryAccount esteja na accountList, o nosso programa vai quebrar. Essa é uma clara violação do LSP, pois temos uma regra mais restrita sendo implementa em uma subclasse, assim a BasicAccount não pode ser substituível pela SalaryAccount.

O LSP completa o princípio aberto/fechado, pois quando criamos subclasses seguindo o LSP, conseguimos fechar ainda mais nossa classe base para modificações.


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

Para mim, é um dos princípios mais simples de aplicar. Ele simplesmente nos incentiva a “fatiar” interfaces “gordas” em interfaces mais granulares. Ter interfaces mais específicas facilita a manutenção e legibilidade do código, além de uma maior coesão.

"Make fine grained interfaces that are client specific".

 Exemplo de violação do ISP

<p> CODE: https://gist.github.com/luissilvazup/cdf27ab098a10209623e5a70399b91eb.js </p>

No exemplo acima temos a interface Car com dois métodos, carregar e abrirPorta, sendo que o primeiro só deve ser implementado por carros elétricos. Assim, temos uma interface muito grande que pode ser quebrada.

Uma possível solução seria criar uma nova interface que deve ser implementada apenas por carros elétricos.

<p> CODE:https://gist.github.com/luissilvazup/5d82e4e8da8115e2e19c51e3aac1178f.js </p>

  • Obs: Tome cuidado ao aplicar esse princípio, sempre reflita se é necessário criar uma interface. Caso contrário, você estará fazendo um overengineering ao criar milhares de interfaces, tentando seguir o princípio ao pé da letra.


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

O último princípio que vamos abordar é o da inversão de dependências, que Uncle Bob definiu em duas regras:

  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.

Para melhor entendermos essas duas regras, vamos analisar o seguinte código

<p> CODE: https://gist.github.com/luissilvazup/ab1653da1097076c7f8026240c5729a8.js </p>

No exemplo, o módulo de alto nível seria a classe AuthenticateLogin, pois ela depende de outros módulos. Pelo fato de criarmos uma nova instância da classe AuthenticationLinkedin dentro do método login, acabamos acoplando as duas classes, deixando nosso design rígido.

Solução com DIP

<p> CODE:https://gist.github.com/luissilvazup/c604d01e75fe6fd5bb503fc9bc1f03a3.js <\p>

Agora todos os nossos módulos dependem da abstração Authentication. Reparem na anotação @AllArgsConstructor do Lombok. Ela cria um construtor no momento da compilação do nosso código e ele recebe como parâmetro uma instância da abstração Authentication. Sendo assim, o cliente que chamar essa classe tem uma maior flexibilidade para trocar a autenticação. O snippet abaixo representa a mesma solução, porém sem utilizar a anotação do Lombok.

<p> CODE:https://gist.github.com/luissilvazup/44036ce61e6a2c38f3d9b810d8a90b81.js</p>

Repare que para a solução desse problema, utilizei um pattern conhecido como injeção de dependência (DI), na qual passo via construtor a dependência que será utilizada na classe AuthenticateLogin. Apesar do nome bem semelhante, DI é totalmente diferente de DIP (Inversão de Dependência). No exemplo, conseguimos seguir o princípio utilizando DI.

Zupcast: Ouça Agora!


Conclusão

Espero que esse artigo ajude a entender melhor o SOLID e que ele seja apenas o início dos seus estudos. Aplicar este princípio no seu código vai trazer uma maior flexibilidade para a arquitetura do seu software. Não se esqueça de deixar suas dúvidas, críticas e opiniões nos comentários :)


Referências:

O que você achou deste conteúdo?
Quer receber nossos conteúdos?
Seu cadastro foi efetuado com sucesso! Enviaremos as novidades no seu email.
Oops! Something went wrong while submitting the form.