Sei que existem 1 milhão de posts sobre SOLID, considero esse "guia" um pouco fora do comum pelo seu aprofundamento, tentei esclarecer todas as dúvidas que tinha ou poderia ter e sempre trazer exemplos, além de usar boas referências. Espero que esse post seja o seu guia definitivo de SOLID, assim como é para mim!
O que é SOLID?
SOLID é um Acrônimo para 5 boas práticas e/ou princípios que envolvem o desenvolvimento de um bom código orientado à objetos, não quero me estender na origem, vamos para os princípios!
Single Responsability Principle
O nome, embora auto-explicativo e que muitas vezes levava esse princípio a ser explicado como “Uma classe deve ter uma, e apenas uma responsabilidade” não é necessariamente o que você deve pensar na hora de implementá-lo. Entenda esse padrão como : “Uma classe deve ter um, e apenas um motivo para mudar”. Ou seja, não crie uma classe com a função de Emitir Nota Fiscal, crie uma classe NotaFiscal, e garanta que apenas coisas que envolvem o domínio de Nota Fiscal irão altera-lá, a mudança da emissão não muda o seu domínio de Nota Fiscal. Se uma alteração na impressora fizer a classe Nota Fiscal ser alterada, algo está incorreto.
A questão principal do SRP é o motivo para uma classe ser modificada. E esse motivo para mudança, em geral, está relacionado a um grupo de usuários ou stakeholders, que Uncle
Bob chama de atores. - Desbravando Solid
Levando em conta o que dizemos acima, também podemos dizer que o SRP pode ser definido por: “Um módulo deve ser responsável por um , e apenas um ator”. O ator que se importa com a função da emissão nota fiscal é o setor de vendas, o ator que se importa com suas horas extras, o rh, o ator que se importa com salvar no banco por id é a implementação de persistência.
Cada "interessado", de negócio ou técnico, faz com que a classe tenha uma responsabilidade diferente.
Identificar classes que mudam por diversos motivos (ou atores) é simples, a coesão pode ser uma das métricas utilizadas:
Classes coesas têm uma característica semelhante: os conceitos que essas classes representam estariam relacionados e separá-los seria pouco natural. O SRP, no fim das contas, é uma outra maneira de falar sobre a necessidade de código coeso. - Desbravando Solid
Aniche (OOP E SOLID para ninjas) sugere que, para encontrar classes pouco coesas, devemos procurar classes que:
- Possuem muitos métodos diferentes.
- São modificadas com frequência.
- Não param nunca de crescer.
Outro fator importante é perceber a duplicação (ou pior, repetição) de código. Se seu código possui partes repetidas diversas vezes, tenha isso como um forte indicativo que essa responsabilidade provavelmente deveria estar encapsulada em algum lugar (e muito provavelmente em uma classe).
Portanto, se seu sistema pega os dados, busca coisas no banco, salva como ePUB ou PDF, tudo em uma classe só, ele provavelmente não é coeso e muito menos segue o SRP. Um exemplo disso é que toda vez que a maneira que um pdf for gerado houver de mudar, você terá que mexer nessa classe principal, e se você tiver que repetir essa alteração em diversos pontos onde o código está repetido (o que já não é um bom indicador), você provavelmente terá problemas em algum momento (e mesmo que não tenha, sua manutenção definitivamente não está facilitada).
Caso real
Recentemente ajudei um amigo em um projeto pessoal, onde ele enviava emails para a confirmação de cadastro de usuários, todas essas responsabilidades ficavam dentro da mesma classe Usuário (Gerar token, enviar email, registrar usuário).
A partir daí, trabalhamos para termos um código mais coeso, no momento em que um usuário é registrado, ele envia um evento de registro de usuário (o que é só um aviso falando: cadastrei um usuário).
Com isso, uma classe chamada enviarEmailListener se prontificava a ouvir eventos de registro e alteração de senha de usuário e ela era a responsável por enviar emails.
Portanto, com o refactor, a classe de Usuário não se preocupa com o que acontece após o registro do usuário, ela apenas notifica que isso aconteceu.
Outro ponto de melhoria nisso foi a possibilidade de tornar o envio do email assíncrono, então para cadastrar um usuário, não precisavamos esperar o serviço de email fazer sua ação, ela é independente (nesse caso, não fazia sentido ser uma transação, o cadastro de um usuário não depende do email, se um erro ocorrer nessa etapa, ele pode só pedir outro email)
Note que se tivermos mais ocasiões onde devemos enviar emails de usuário (além de cadastro e alteração de emails) podemos apenas adicionar um novo evento a ser ouvido pelo listener e se precisarmos mudar a forma de enviar email, mudamos apenas em um único método (o que aumenta MUITO a capacidade de manter o código), essa é a função do SRP.
Open-Closed Principle
Software entities ... should be open for extension, but closed for modification.
A module will be said to be open if it is still available for extension. For example, it should be possible to add fields to the data structures it contains, or new elements to the set of functions it performs.
Essas citações trazem uma noção base do que o OCP quer dizer, nosso código deve estar sempre pronto para evoluir. e de maneira natural, não devemos sentir a necessidade de modificar muitos arquivos diferentes, ou mesmo procurar (usando o CTRL+F, por exemplo) os lugares que devem ser alterados.
A segunda parte diz sobre ser fechada para modificação. nesse aspecto, podemos entender que elas não devem ter seu comportamento alterado a todo momento.
Um exemplo
public class CalculadoraDePrecos {
public double calcula(Compra produto) {
Frete correios = new Frete();
double desconto = 0.0; // Inicialize desconto com um valor padrão
if (REGRA 1) {
// faz algo
}
if (REGRA 2) {
// faz algo
}
return produto.getValor() * (1 - desconto) + frete;
}
}
Note que sempre que adicionarmos regras de frete, nosso código precisará alterar a classe calculadora de preços, incluindo um novo IF, ou seja, a classe que implementar a regra de frete está acoplada à Calculadora de Preços
Uma segunda implementação comum é colocar os ifs dentro das classes
específicas. Por exemplo, a classe Frete passaria a ter as diferentes regras de negócio (os mesmos if-else). Apesar de ficar melhor, continua claro que não é a melhor abordagem.
Melhorando
Se temos diferentes regras de desconto e de frete, basta criarmos interfaces que as representam:
public interface TabelaDePreco {
double descontoPara(double valor);
}
public class TabelaDePreco1 implements TabelaDePreco { }
public class TabelaDePreco2 implements TabelaDePreco { }
public class TabelaDePreco3 implements TabelaDePreco { }
public interface ServicoDeEntrega {
double para(String cidade);
}
public class Frete1 implements ServicoDeEntrega {}
public class Frete2 implements ServicoDeEntrega {}
public class Frete3 implements ServicoDeEntrega {}
"Veja que essa simples mudança altera toda a maneira de se lidar com a classe. Com ela 'aberta', ou seja, recebendo as dependências pelo construtor, podemos passar a implementação concreta que quisermos para ela. Se passarmos a implementação TabelaDePreco1, e invocarmos o método
calcula(), o resultado será um; se passarmos a implementação TabelaDePreco2 e invocarmos o mesmo método, o resultado será outro." (ANICHE, 2015).
public class CalculadoraDePrecos {
private TabelaDePreco tabela;
private ServicoDeEntrega entrega;
public CalculadoraDePrecos(TabelaDePreco tabela, ServicoDeEntrega entrega) {
this.tabela = tabela;
this.entrega = entrega;
}
public double calcula(Compra produto) {
double desconto = tabela.descontoPara(produto.getValor());
double frete = entrega.para(produto.getCidade());
return produto.getValor() * (1 - desconto) + frete;
}
}
💡 Exemplo do Livro OOP e SOLID para Ninjas
Note que aqui trabalhamos também com a inversão de dependências (DIP), o “D”, do SOLID.
"Se o OCP declara o objetivo de uma arquitetura OO, o DIP declara o seu mecanismo fundamental."
Se você ainda não leu sobre o DIP, continue lendo o post, mais tarde revisite esse ponto, vai fazer sentido 😃
Abstrações e Capacidade de Extensão
"O que discutimos aqui, de certa forma, mistura-se com a discussão do capítulo anterior sobre estabilidade e inversão de dependências. As interfaces (abstrações)
TabelaDePrecoeServicoDeEntregatendem a ser estáveis. ACalculadoraDePrecosé uma implementação mais instável e que só depende de abstrações estáveis. Pensar em abstrações nos ajuda a resolver o problema do acoplamento e, de quebra, ainda nos ajuda a ter códigos facilmente extensíveis. Isso é programar orientado a objetos. É lidar com acoplamento, coesão, pensando em abstrações para nossos problemas. Quando se tem uma boa abstração, é fácil evoluir o sistema. Seu sistema deve evoluir por meio de novas implementações dessas abstrações, previamente pensadas, e não por meio de diversos ifs espalhados por todo o código." (ANICHE, 2015).
Design Pattern: Command
Não vou me aprofundar em patterns nesse post, mas acho legal repassar alguns exemplos que vi no material base
public class EmissorNotaFiscal {
private RegrasDeTributacao tributacao;
private LegislacaoFiscal legislacao;
private List<AcaoPosEmissao> acoes;
//...
public NotaFiscal gera(Fatura fatura) {
List<Imposto> impostos = tributacao.verifica(fatura);
List<Isencao> isencoes = legislacao.analisa(fatura);
// método auxiliar
NotaFiscal nota = aplica(impostos, isencoes);