Guia de S.O.L.I.D. com Java

Guia de S.O.L.I.D. com Java

SOLID é importante para qualquer linguagem de programação que adote o paradigma de orientação a objetos, eles facilitam o processo de desenvolvimento, como por exemplo na manutenção e escalabilidade do código, visando boas práticas e melhorando a vida útil de nós programadores. Este post é focado para quem não sabe sobre o significado de SOLID e também para quem sabe mas procura um exemplo na linguagem Java, não irei focar no paradigma de orientação a objetos pois não é foco, porém, se você não conhece, é importante que visite este link para que compreenda melhor este post.

O que é SOLID?

SOLID é um acrônimo idealizado por Micheal Feathers que unificou os princípios da orientação a objetos apresentados inicialmente por Robert C Martin, mais conhecido como “Uncle Bob” em seu artigo chamado “The principles of OoD” de 1995, a sigla significa:

Mais abaixo irei explicar com detalhes o que cada letra dessas significa tanto na teoria quanto na prática com Java, explorando exemplos também com imagens.

S – Single Responsability Principle (Princípio da Responsabilidade Única)

Este princípio diz que cada método, função ou classe, deve ter uma responsabilidade única, ou seja, deve ter um único objetivo.

Como a imagem acima demonstra, neste princípio, cada um tem uma única função, sendo uma má prática alguma classe ou método fazer mais de uma coisa ao mesmo tempo, vamos ver o exemplo em código, imaginando um cenário de um método que gera senhas, verifica e salva no banco de dados:

public String geraVerificaESalvaSenha() {
// Implementação do código…
}

Problemática

Apesar de ter um nome que diz exatamente o que o método faz, como uma boa prática de código limpo, o método faz muitas coisas ao mesmo tempo, se alguma dessas etapas estiverem com bug, fica mais difícil de identificar, é importante que separe tudo para que não se tenha dupla ou mais funções.

Solução

Separar cada etapa do método acima em diferentes métodos.

public String gerarSenha() {
// Implementação do código…
}

public void verificarSenha(String senha) {
// Implementação do código…
}

public void salvarSenha(String senha) {
// Implementação do código…
}

Com isso, é mais fácil de identificar um possível erro que venha a acontecer, podendo até mais facilmente no futuro separar estes métodos para diferentes classes.

Uma forma de identificar se um método está fazendo mais de uma função é se atentar ao seu nome, como no exemplo acima, para classes, é importante prestar atenção se seus métodos implementados condizem com sua abstração, como por exemplo:

public class Copo {

public void notificarEstado() {
// Implementação do código…
}

public void limpar() {
// Implementação do código…
}

public void encher() {
// Implementação do código…
}

public void esvaziar() {
// Implementação do código…
}

}

Problemática

Podemos ver no exemplo acima que temos métodos que não precisam estar implementados nesta classe, como por exemplo, notificarEstado para saber se um copo está vazio ou cheio ou o método limpar que serve para limpar o copo, estes métodos não fazem sentido na abstração de um Copo, pois abragem muito mais coisas.

Solução

Delegar os métodos que não fazem parte de uma abstração para uma outra que faça mais sentido, como podemos ver abaixo:

public class Notificação {
public void notificar() {
// Implementação do código…
}
}
public class LavaLoucas {
public void limpar() {
// Implementação do código…
}
}
public class Copo {
public void encher() {
// Implementação do código…
}

public void esvaziar() {
// Implementação do código…
}
}

Por mais que as classes sejam bem pequenas inicialmente, é mais fácil para expandir e podermos implementar outras classes que possam utilizar da notificação e de serem limpos também, assim a classe Copo fica responsável somente por aquilo que faz sentido para sua abstração.

O – Open-Closed Principle (Princípio Aberto-Fechado)

Este princípio diz que suas classes e métodos/funções devem estar abertos para extensões e fechados para modificações, ou seja, se você quer realizar adições as suas classes/métodos, não é recomendável que se mexa em seu comportamento, isso evita que gere possíveis bugs a cada código novo adicionado.

A imagem acima representa o nosso exemplo que veremos logo abaixo.

public class CLT {
public int salario() {
// Implementação do código…
}
}
public class Estagio {
public int bolsaAuxilio() {
// Implementação do código…
}
}
public class FolhaDePagamento {
private int saldo;

public int calcular(Object funcionario) {
if (funcionario instanceof CLT) {
this.saldo = ((CLT) funcionario).salario();;
} else if (funcionario instanceof Estagio) {
this.saldo = ((Estagio) funcionario).bolsaAuxilio();
}
}
}

Problemática

Neste exemplo acima, a medida que formos adicionando novos tipos de funcionários, novas verificações seriam necessárias e estaríamos mudando o comportamento do método.

Solução

Podemos observar que tanto o funcionário CLT quanto o estagiário, recebem uma remuneração, sendo assim, podemos simplificar os dois como uma interface que atenda os dois, deste jeito:

public interface Funcionario {
int remuneracao();
}
public class CLT implements Funcionario {

@Override
public int remuneracao() {
// Implementação do código…
}
}

public class Estagio implements Funcionario {

@Override
public int remuneracao() {
// Implementação do código…
}
}

public class FolhaDePagamento {
private int saldo;

public int calcular(Funcionario funcionario) {
this.saldo = funcionario.remuneracao();
}
}

Com a criação desta simples interface, foi possível que simplificássemos nosso método calcular podendo assim receber qualquer tipo de funcionário, basta que implemente a interface Funcionário criando o método de remuneração, sendo assim, não precisa ser alterado caso queiramos adicionar mais tipos de funcionários.

L – Liskov Substitution Principle (Princípio da Substituição de Liskov)

A ideia deste princípio surge de uma cientista americana chamada Barbara Liskov que diz que as classes derivadas devem ser substituíveis pelas suas classes bases.

Esta ideia é a mesma da herança, a classe-filha deve ter/fazer tudo que a sua classe-mãe tem/faz, podendo ser substituída, não criando bugs por não ter implementado.

Para este princípio, trouxe um exemplo de um mediaplayer que tem como objetivo reproduzir qualquer tipo de mídia, um áudio, vídeo, etc.

public class MediaPlayer {
public void reprodruzir() {
System.out.println(“Reproduzindo mídia…”);
}
}
public class Mp3Player {
public void reprodruzir() {
System.out.println(“Reproduzindo som…”);
}
}
public class Mp4Player {
public void reprodruzir() {
System.out.println(“Reproduzindo vídeo…”);
}
}

Veja que podemos alternar entre as classes dos players de mp3 e mp4 que irão fazer a mesma função que sua classe-mãe e podem ser substituídas pela sua classe base.

I – Interface Segregation Principle (Princípio da Segregação de Interface)

O princípio da segregação de interface diz que uma classe filha não pode ser forçada a implementar métodos dos quais ela não irá utilizar.

Podemos ver mais abaixo, um exemplo onde temos um robô e um humano que irão executar um trabalho, classe essa que foi definida para que ambos executem estas tarefas, já que são suas filhas.

public interface Trabalho {
void comecarATrabalhar();

void almocar();

void carregarBateria();

void terminarTrabalho();
}

public class Robo implements Trabalho {
@Override
public void comecarATrabalhar() {
// Implementação do código…
}

@Override
public void almocar() {
return null;
}

@Override
public void carregarBateria() {
// Implementação do código…
}

@Override
public void terminarTrabalho() {
// Implementação do código…
}
}

public class Humano extends Trabalho {
@Override
public void comecarATrabalhar() {
// Implementação do código…
}

@Override
public void almocar() {
// Implementação do código…
}

@Override
public void carregarBateria() {
return null;
}

@Override
public void terminarTrabalho() {
// Implementação do código…
}
}

Problemática

Existem coisas que não são feitas por humanos como carregar a bateria e nem um robô que iria almoçar, ou seja, não é recomendado que se tenha métodos que retornem nulo pois foram forçados a serem implementados.

Solução

Basta que determine os métodos que apenas serão usados por todos e deixem os mais especifícos para que sejam exclusivos de suas implementações, como podemos ver abaixo:

public class Trabalho {
public void comecarATrabalhar() {
// Implementação de código…
}

public void terminarTrabalho() {
// Implementação de código…
}
}

public class Robo extends Trabalho {
public void carregarBateria() {
// Implementação de código…
}
}
public class Humano extends Trabalho {
public void almocar() {
// Implementação de código…
}
}

D – Dependecy Inversion Principle (Princípio da Inversão de Dependência)

Este princípio se baseia na ideia de que abstrações não devem depender de detalhes e sim os detalhes que devem depender de abstrações.

Esta definição pode ser reescrita de outra forma, como, módulos de alto nível não devem de módulos de baixo de baixo nível, ambos devem depender de abstrações.

Caso não tenha ficado claro ainda, abaixo temos um exemplo.

Neste exemplo, temos um serviço que envia e-mails utilizando uma biblioteca chamada JavaMailSender.

public class EmailService {
private final JavaMailSender mailSender;

public EmailService(JavaMailSender mailSender) {
this.mailSender = mailSender;
}

public void enviarEmail(String emailTo, String subject, String body) throws MailException {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(emailTo);
message.setSubject(subject);
message.setText(body);

mailSender.send(message);
}
}

Problemática

O problema desse código está na dependência na biblioteca JavaMailSender, imagine que a biblioteca seja descontinuada ou qualquer outro tipo de problema, precisáriamos reescrever toda a classe para trocar esta biblioteca, trocar para o Amazon SES, mailgun.

Solução

Basta adicionar uma interface que faz o JavaMailSender conversar com o serviço criando o método por ele necessário, agora se quisermos trocar para uma implementação utilizando Amazon SES ou mailgun, bastaria criar outra classe que implementasse esta interface chamada EmailSenderGateway.

public interface EmailSenderGateway {
void enviarEmail(String emailTo, String subject, String body);
}
public class GmailSender implements EmailSenderGateway {

private final JavaMailSender mailSender;

public EmailService(JavaMailSender mailSender) {
this.mailSender = mailSender;
}

@Override
public void enviarEmail(String emailTo, String subject, String body) throws MailException {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(emailTo);
message.setSubject(subject);
message.setText(body);

mailSender.send(message);
}
}

public class EmailService {
private final EmailSenderGateway emailSenderGateway;

public EmailService(EmailSenderGateway emailSenderGateway) {
this.emailSenderGateway = emailSenderGateway;
}

public void enviarEmail(String emailTo, String subject, String body) {
this.emailSenderGateway.enviarEmail(emailTo, subject, body);
}
}

Com isso, o serviço não depende de ninguém e nem conhece as implementações mais acima, apenas conhece a interface, assim como suas implementações, tornando o código mais flexível.

Saiba mais

Deixo aqui uns vídeos para caso você queira se aprofundar mais no assunto, recomendo que leia os livros do Robert C Martin, o “Uncle Bob”, pois ele difundiu muito dessas ideias e é referência na nossa área.

Referências

Alura
Trybe
Ugonna Thelma

Leave a Reply

Your email address will not be published. Required fields are marked *