SOLID na Prática: Conheça e aprenda a aplicar os princípios.
Publicado por Raul Neto em 20/04/2022
Você conhece os princípios SOLID? Confira neste artigo quais são e como aplicá-los no dia a dia para ter um código limpo na prática!
Criar códigos de qualidade durante todo o processo de desenvolvimento é um desafio e tanto na carreira do desenvolvedor de software. Usar boas práticas de programação tem a finalidade de reduzir duplicações de código, a responsabilidade das entidades, a complexidade do código, ocasionando assim uma melhor qualidade no código-fonte.
Na programação orientada a objetos (POO) temos diversos princípios e ferramentas que garantem a qualidade do seu código.
Neste artigo abordaremos o SOLID e como podemos aplicá-lo na prática.
Princípio SOLID
SOLID é um conjunto de cinco princípios que visam garantir a qualidade no desenvolvimento de software, tornando seu sistema mais fácil de testar, manter, escalar e testar. Trata-se de um acrônimo dos princípios criados por Robert C. Martin (a.k.a Uncle Bob) e abordados no artigo The Principles of OOD.
Quais são os princípios SOLID
S - Single Responsability Principle ou Princípio da Responsabilidade Única.
O - Open-closed Principle ou Princípio Aberto-Fechado.
L - Liskov Substitution Principle ou Princípio de Substituição de Liskov.
I - Interface Segregation Principle ou Princípio de Segregação de Interface.
D - Dependency Inversion Principle ou Princípio da Inversão de Dependência
Neste artigo abordaremos cada um dos itens com exemplos práticos.
Legenda: SOLID é um conjunto de cinco princípios que visam garantir a qualidade no desenvolvimento de software. | Imagem: Unsplash
1 - SRP - Single Responsability Principle
O princípio da responsabilidade única determina que uma classe deve ter um e somente um motivo para mudar.
Este princípio declara que uma classe deve ter apenas uma responsabilidade dentro do desenvolvimento de software, ou seja, ela deve ter apenas uma única tarefa ou função para executar.
Quando estamos aprendendo a desenvolver nossos primeiros sistemas, geralmente atribuímos várias funções dentro de uma mesma classe. Em um primeiro momento esta abordagem pode se mostrar eficiente, porém torna-se muito difícil realizar qualquer tipo de alteração sem comprometer o funcionamento da classe.
Exemplo da classe Pedido
A classe Pedido está violando as regras do Single Responsability Principle pois está executando 3 funções diferentes: a regra de negócio, manipulação da base de dados e geração de uma renderização para o usuário.
Aplicando o princípio na classe, teremos o código neste formato:
Exemplo da classe PedidoRepository
Exemplo da classe PedidoView
Com esta refatoração, agora temos 3 classes distintas, sendo que cada uma delas é responsável por apenas uma função.
2 - OCP - Open-closed Principle
O princípio aberto-fechado determina que uma entidade deve estar aberta para a extensão e fechada para a modificação.
Na prática, quando realizamos alguma alteração de recurso ou de comportamento da entidade, devemos estender este novo recurso e comportamento e não fazer alterações no código-fonte.
Vamos simular um pequeno sistema que realiza o cálculo de preços de um produto, considerando valor e frete. A implementação deste sistema se encontra abaixo:
Exemplo da classe CalculadoraPreco
O método calcula() da classe Calculadora preço verifica as regras de desconto e de frete a partir do meio de pagamento informado. Se o produto for comprado à vista, o desconto é calculado a partir da tabela TabelaPrecoAVista. Caso for comprado à prazo, o desconto é calculado a partir da tabela TabelaPrecoAPrazo.
Além disso, as classes dentro das tabelas implementam uma política de preços baseado no produto e o frete varia por estado:
Exemplo da classe TabelaDePrecoAPrazo
O problema desta implementação é a complexidade da regra de negócio. Além disso, o acoplamento de código é muito grande, pois uma classe está dependendo da outra para seu funcionamento.
Aplicando o princípio aberto-fechado, teremos um código parecido com este:
Com esta refatoração, a classe CalculadoraDePrecos não precisa conhecer a implementação da política de preços nem do cálculo do frete, pois este é um comportamento que a classe deve ter. Caso seja necessário implementar novos comportamentos, basta adicionarmos este comportamento através de interfaces e implementá-las na classe em questão.
3 - LSP - Liskov Substitution Principle
O princípio da Substituição de Liskov determina que uma classe derivada deve ser substituível por sua classe base.
Na prática, todas as classes filhas (que foram implementadas através de uma herança) devem manter os mesmos comportamentos da classe pai.
Vamos a um exemplo de um código que implementa este princípio:
Como a classe B herda o comportamento da classe A, ao utilizar a função imprimeNome() o código funcionará da mesma forma.
Devemos, no entanto, nos atentar a não violar este princípio, principalmente se:
Sobrescrever/implementar um método que não faz nada.
Lançar uma exceção
Retornar tipos e valores diferentes da classe pai.
Para não violar o LSP, além de estruturar muito bem as suas abstrações, será necessário implementar Injeção de Dependência e também outros princípios do SOLID, como o Open-closed Principle e o Interface Segregation Principle (veremos a seguir).
4 - ISP - Interface Segregation Principle
O princípio de segregação de interface determina que uma classe não deve ser forçada a implementar interfaces que possuem atributos e métodos que não irão utilizar.
Na prática, devemos preferir criar interfaces mais específicas ao invés de interfaces genéricas.
Vamos simular que estamos desenvolvendo um jogo que possui aves como personagem. Neste jogo implementamos uma interface Aves para abstrair o comportamento deste personagem, e faremos com que as classes implementem esta interface:
Exemplo da interface IAves
Exemplo da classe Pombo
Exemplo da classe Galinha
Percebam que na classe Galinha não podemos implementar o método setAltitude(), pois a galinha não voa! Isso acontece, pois criamos uma interface genérica para as aves e acabamos violando tanto o ISP quando o LSP.
E como podemos corrigir este problema? Devemos criar interfaces mais específicas, como o exemplo abaixo:
Exemplo da aplicação do ISP
No exemplo acima, movemos o método setAltitude() para a interface IAvesQueVoam. Desta forma conseguimos isolar os comportamentos específicos dos personagens do jogo, respeitando o princípio ISP.
5 - DIP - Dependency Inversion Principle
O princípio da Inversão de Dependência determina que uma classe deve depender de abstrações, mas nunca de implementações.
Uncle Bob define este princípio da seguinte forma:
"Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender da abstração."
"Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações."
Complexo não é? Vamos entender este conceito a partir de um exemplo.
Vamos supor que possuímos uma classe de lembrete de senha que precisa se conectar a uma base de dados, conforme abaixo:
Exemplo da classe PasswordReminder
Neste trecho de código vemos que a classe PasswordReminder possui um alto níve de acopamento, pois depende da instância da classe PostSQLConnection para se conectar a base de dados. Se fosse necessário reaproveitar a conexão do banco PostgreSQL precisamos levar a implementação da conexão para a outra classe.
Como aplicamos o Dependency Inversion Principle?
Vamos refatorar o código da classe PasswordReminder para que ele dependa da abstração proposta pela interface de conexão:
Exemplo da classe PasswordReminder após aplicar o DIP
Veja que após a refatoração, o construtor da classe PasswordReminder agora depende de uma abstração de conexão ao banco de dados. Para ela, não importa qual banco de dados irá conectar nesta classe, ela precisa somente que faça a conexão a uma base de dados!
Além disso, estamos favorecendo a reusabilidade de código e ainda estamos respeitando os princípios SRP e OCP.
E aí, curtiu esse conteúdo? Deixa nos comentários sua opinião, e vamos trocar ideias lá no fórum também, acesse o link pelo botão abaixo:
Conheça nosso fórum!
Utilizar os princípios SOLID tornam o nosso software mais robusto, escalável, flexível e tolerante a mudanças, pois facilita a implementação de novos requisitos para evolução e manutenção do sistema.
No começo pode ser um pouco assustador entender todos estes princípios e aplicá-los no nosso processo de desenvolvimento. Vale lembrar que a utilização destes princípios variam de caso a caso, mas com a prática e a constância, evoluimos o nosso código, tornando-os cada vez mais maduros.
Desenvolvedor Backend na Tecnospeed, com especialização em NodeJS e um apaixonado por código e games.