Durante a vida útil de um software, a maior parte do custo do desenvolvimento de software é gasto na manutenção do software. Então, como desenvolvedores, queremos construir algo que não seja frágil, mas fácil de manter. E utilizar algumas regrinhas do SOLID pode nos ajudar a chegar nesse nível
Single Responsability Principle
public class SonoplastaRodrigoFaro {
// Audio é um texto em representação aos bordões
private List<String> audios = new ArrayList<>();
private Integer count = 0;
public void addAudio(String audio) {
StringBuilder stringBuilder = new StringBuilder();
String stringNova = stringBuilder
.append(++count)
.append(":")
.append(audio)
.toString();
audios.add(stringNova);
}
public void removeAudio(String audio) {
audios.removeIf(a -> a.equals(audio));
}
public List<String> getAudios() {
return audios;
}
public Optional<String> getAudio(String audio) {
return audios.stream()
.filter(a -> a.equals(audio))
.findFirst();
}
}
}
Temos aqui a classe Sonoplasta, que tem como atributos um contador de entradas e uma Lista de audios, e métodos fazem os CRUD desses respectivos áudios.
É uma classe com responsabilidades de Sonoplastia apenas, não há persistência e nem outras responsábilidades.
Código da persistência:
public class SonoplastaPersistence {
public final static String PATH = "/home/pedro/Documentos/audios.txt";
public void persist(List<String> audios) throws IOException {
try(BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(PATH))) {
String entradas = audios
.stream()
.map(item -> String.valueOf(item))
.collect(Collectors.joining("|"));
bufferedWriter.write(entradas);
System.out.println("Persistido com sucesso!");
} catch (IOException e) {
throw new IOException("Error on persist the archive");
}
}
}
Classe de Persistência que gera um arquivo em um diretório de sua preferencia com as anotações do seu Journal.
public class SRP {
public static void main(String[] args) throws IOException {
SonoplastaRodrigoFaro sonoplasta = new SonoplastaRodrigoFaro();
sonoplasta.addAudio("CAVALOO");
sonoplasta.addAudio("UUUIII");
sonoplasta.addAudio("ELE GOXXTA");
sonoplasta.addAudio("Que isso meu fio calma");
sonoplasta.getAudios()
.forEach(System.out::println);
sonoplasta.getAudio("1:CAVALOO")
.ifPresentOrElse(audio -> System.out.println("Audio: " + audio), () -> System.out.println("Audio não existe"));
SonoplastaPersistence sonoplastaPersistence = new SonoplastaPersistence();
sonoplastaPersistence.persist(sonoplasta.getAudios());
}
}
E por fim, outra classe que faz a chamada dessas duas classes.
Demonstração do funcionamento:
Arquivo persistido:
Open Closed Principle
Esse é um princípio que é bem discutido. Ele diz que uma classe DEVE estar fechada para IMPLEMENTAÇÃO porém ABERTA para EXTENSÃO. Isso quer dizer: Uma classe quando feita, não deve ser modificada e sim herdada, isso gera um certo desconforto, porque não podemos alterar nossas classes? O jeito para seguirmos bem esse principio é utilizando de Abstrações, sendo classes abstratas ou até mesmo interface. Mostrarei duas formas de utilizar esse principio.
Provavelmente, você ja criou uma calculadora na vida, e nessa calculadora devia ser algo tipo:
if (action == "+") {
doSum();
} else if (action == "-") {
doSubtraction();
}
e daí vai. Isso fere o OCP, porque? se criarmos a classe de calculo com esses dois métodos, teriamos mais dois para implementar depois, que seriam a divisão e multiplicação, estariamos alterando nossa classe ja pronta. Como podemos resolver isso?
Primeiro, vamos criar uma interface chamada ICalculation:
public interface ICalculation {
void perform();
}
ela possui um método chamado perform que atuará para todas as operações básicas.
Agora criaremos uma classe para cada operação básica, implementando a interface.
public class Multiplication implements ICalculation{
private double left;
private double right;
private double result;
@Override
public void perform() {
result = left * right;
System.out.println("Result of multiplication: " + result);
}
public Multiplication(double left, double right) {
this.left = left;
this.right = right;
this.result = 0.0;
}
public double getLeft() {
return left;
}
public void setLeft(double left) {
this.left = left;
}
public double getRight() {
return right;
}
public void setRight(double right) {
this.right = right;
}
}
faça isso para cada operação básica, apenas alterando o perform(). Percebemos que ao fazer isso, nosso código está genérico e funcionará com base nas implementações de cada operação. Exemplo:
public class OCP {
public static void main(String[] args) {
ICalculation calculation = new Multiplication(2, 2);
calculation.perform();
}
}
Podemos ver que não precisamos fazer várias estruturas de decisões para cada uma operação, basta passarmos uma instancia da implementação que queremos. Um exemplo disso acima está sendo passada a multiplicação entre dois números, se quisessemos uma soma, passariamos uma instancia de Soma e assim por diante.
Lembre-se, não leve esse principio ao pé da letra.
Specification Pattern funciona muito bem com OCP
Vamos utilizar outro exemplo pondo em prática OCP com um padrão chamado Specification.
obs: instalei o lombok, pois como faremos tudo na mesma classe devido ao tamanho do exemplo, para não ficar trocentas linhas na página estaremos utilizando as anotações do lombok para getters, setters e constructors.
public class FilterBySpecification {
public static void main(String[] args) {
var iphone = new Product(6.7, "iphone", 1800);
var motorola = new Product(6.8, "motorola", 900);
var iphoneX = new Product(6.9, "iphone", 3800);
var listProducts = List.of(iphone, motorola, iphoneX);
FilterProduct filterProduct = new FilterProduct();
filterProduct.filterByName(listProducts, "iphone")
.forEach(System.out::println);
}
static class FilterProduct {
public List<Product> filterByName(List<Product> products, String name) {
return products.stream()
.filter(product -> product.getName().equals(name))
.collect(Collectors.toList());
}
}
@Data
@AllArgsConstructor
@ToString
static class Product {
public double size;
public String name;
public double price;
}
}
Logo acima, temos 3 classes simples, uma de produtos, uma para o método maine outra para a filtragem de produtos. podemos ver que ja filtramos produtos pelo nome, isso está certo, porém, se o seu chefe te pedir para filtrar por PREÇO também? depois por Tamanho? depois por Preço e Tamanho, e assim por diante? você adicionaria mais métodos dentro da classe ja pronta? um filterBySize? Bom, funcionaria porém fere o Open Closed Principle, desta forma, podemos implementar usando um padrão chamado Specification:
public class FilterBySpecification {
interface Specification<T> {
boolean isSatisfied(T item);
}
public static void main(String[] args) {
var iphone = new Product(6.7, "iphone", 1800);
var motorola = new Product(6.8, "motorola", 900);
var iphoneX = new Product(6.9, "iphone", 3800);
var listProducts = List.of(iphone, motorola, iphoneX);
FilterItens filterProduct = new FilterItens();
filterProduct.filterItem(listProducts, new NameSpecification("iphone"))
.forEach(System.out::println);
}
interface Filter<T> {
Stream<T> filterItem(List<T> itens, Specification<T> specification);
}
static class FilterItens implements Filter<Product> {
@Override
public Stream<Product> filterItem(List<Product> itens, Specification<Product> specification) {
return itens.stream()
.filter(specification::isSatisfied);
}
}
@AllArgsConstructor
static class NameSpecification implements Specification<Product> {
private String name;
@Override
public boolean isSatisfied(Product item) {
return item.getName().equals(name);
}
}
@Data
@AllArgsConstructor
@ToString
static class Product {
public double size;
public String name;
public double price;
}
}
O que fazemos aqui: Nós criamos uma interface genérica com o nome de Specification que possui um método chamado isSatisfied() que retorna um boolean, nós implementaremos esse método em nossos specifications que são, Size, Price e Name. Esse método servirá para filtrarmos nossos itens para cada specification. E criamos também uma classe chamada FilterItens que recebe uma lista de produtos e um Specification, podendo ser size, price, name, nós podemos fazer todos esses filtros usando somente UM método, respeitando o open closed principle. Se surgir uma nova necessidade de filtro, basta apenas criar uma nova Specification, como por exemplo: SizeSpecification e fazer o filtro por SizeSpecification ao invés de NameSpecification. isso fará com que não mexemos na nossa classe que tem a regra de negócio, de filtro, respeitando o principio. Exemplo do código em funcionamento:
Bi ou mais Specifications: Também é possível fazer com que seja comparados UMA ou MAIS Specifications por filtro, por exemplo, quero filtrar um produto por PREÇO e por NOME ao mesmo tempo. Exemplo:
Em nosso código, será adicionado mais uma interface e uma classe:
interface BiFilter<T> {
Stream<T> filterItens(List<T> itens, Specification<T> specification1, Specification<T> specification2);
}
static class BiFilterItens implements BiFilter<Product> {
@Override
public Stream<Product> filterItens(List<Product> itens, Specification<Product> specification1, Specification<Product> specification2) {
return itens.stream()
.filter(item -> specification1.isSatisfied(item) && specification2.isSatisfied(item));
}
}
Ele fará com que seja verificado as duas Specifications no mesmo filtro. Implemente isso no método main da seguinte forma:
public static void main(String[] args) {
var iphone = new Product(6.7, "iphone", 1800);
var motorola = new Product(6.8, "motorola", 900);
var iphoneX = new Product(6.9, "iphone", 3800);
var listProducts = List.of(iphone, motorola, iphoneX);
BiFilterItens filterProduct = new BiFilterItens();
filterProduct.filterItens(listProducts, new NameSpecification("iphone"), new SizeSpecification(6.9))
.forEach(System.out::println);
}
Perceba que criamos uma nova Specification chamada Size, onde será feito a lógica para o tamanho
@AllArgsConstructor
static class SizeSpecification implements Specification<Product> {
private double size;
@Override
public boolean isSatisfied(Product item) {
return item.getSize() == size;
}
}
E estamos passando esse SizeSpecification como segundo parametro para o nosso filterItens.
Resultado do código:
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 (Classe sendo herdada).
Vamos supor que temos uma classe Pessoa, com CPF, RG, Nome e uma classe Aluno com os mesmos atributos da classe Pessoa porém com Média e Faltas, percebeu que podemos fazer Aluno herdar Pessoa?
Devemos, no entanto, nos atentar a não violar este princípio, principalmente se:
- Sobrescrever/implementar um método que não faz nada.
- Retornar tipos e valores diferentes da classe pai.
Por exemplo, se na classe pai, cpf retorna String, na classe filho, cpf não pode retornar um Long. Pense como se os métodos da classe pai fosse um contrato de polimorfismo para a classe filha.
De fato, foi um exemplo bem razo, vamos tentar pensar em algo mais usual no dia a dia, como em midia sociais.
Vamos a seguinte classe pai:
public abstract class MediaSocial {
void chamadaEmGrupo() {
// Faça algo
}
void publicarPost() {
// faça algo
}
void criarGrupos () {
// faça algo
}
void chatComAmigo() {
// faça algo
}
}
Vemos que temos uma classe chamada Media SOcial que tem todos as funcionalidades de uma rede social, como o facebook correto?
public class Facebook extends MediaSocial{
void chamadaEmGrupo() {
// Faça algo
}
void publicarPost() {
// faça algo
}
void criarGrupos () {
// faça algo
}
void chatComAmigo() {
// faça algo
}
}
Podemos observar que o facebook implementa todos os métodos da classe MediaSocial, portanto não há violação do principio, pois um pode substituir o outro sem riscos de quebra de contrato.
Agora e se o Whatsapp herdasse Social Media?
public class Whatsapp extends MediaSocial{
void chamadaEmGrupo() {
// Faça algo
}
void publicarPost() {
// faça algo
}
void criarGrupos () {
// faça algo
}
void chatComAmigo() {
// faça algo
}
}
Podemos notar que Whatsapp não tem Publicação de Postagens, então... boom! Portanto, a classe filha whatsapp não substitui a classe pai. Então não segue o principio do LSP
Mas como podemos resolver isso? Vou te mostrar na apresentação da letrinha I (Interface Segregation) do SOLID logo abaixo:
** Obs: Lembrando que, se possível evite fazer usos de Herança, use e abuse de interfaces, Assim como outros princípios do SOLID, o LSP pode se tornar um antipadrão — o que significa que, às vezes, a complexidade adicional não vale a compensação. Exceto em certos casos **
Interface Segregation
Você percebeu, que no exemplo acima, a classe abstrata MediaSocial está implementando trocentos métodos fazendo com que, Whatsapp, Instagram e outras redes sociais não possam implementá-la devida a quantidade de método presente nesse recurso?
Então vamos segregar as interfaces e deixar a parada mais bonita e usável.
Primeiro, vamos arrancar fora essa classe MediaSocial e criar uma interface chamada MediaSocial
interface MediaSocial {
void chatComAmigo();
void chamadaDeVoz();
}
Essa interface, ela possui apenas uma responsabilidade, que se trata dos chats privados
Outra interface chamada MediaGrupo:
interface MediaGrupos {
void chamadaEmGrupo();
void criarGrupos ();
}
Que possui responsabilidades de Grupos de pessoas.
e outra interface chamada MediaFeed:
interface MediaFeed {
void publicarPost();
void verPosts();
}
Que é responsável pelo Feed de noticias das pessoas.
Agora, vamos ajustar as classes Facebook e Whatsapp:
public class Facebook implements MediaSocial, MediaGrupos, MediaFeed{
@Override
public void publicarPost() {
}
@Override
public void verPosts() {
}
@Override
public void chamadaEmGrupo() {
}
@Override
public void criarGrupos() {
}
@Override
public void chatComAmigo() {
}
@Override
public void chamadaDeVoz() {
}
}
Como o Facebook possui todas as funções das interfaces que segregamos, ele implementará todas, Já o whatsapp:
public class Whatsapp implements MediaGrupos, MediaSocial{
@Override
public void chamadaEmGrupo() {
}
@Override
public void criarGrupos() {
}
@Override
public void chatComAmigo() {
}
@Override
public void chamadaDeVoz() {
}
}
Só implementará DUAS interfaces, pois whatsapp não tem Feed de noticias.
Percebeu o ganho e abstração que tivemos implementando esse principio? Um dos melhores principios para sua vida!!
Dependency Inversion
Dependency Inversion basicamente diz que é pra você não depender de uma classe concreta e sim de uma abstração, Módulos de Alto nível não devem depender de módulos de baixo nível, ambos devem depender de abstração.
Bom, vamos criar uma classe que simula um pagamento, que teremos a opção de: Boleto Bancario, Cartões de crédito e débito.
public class Payment {
private CartaoCredito cartaoCredito;
private BoletoBancario boletoBancario;
private DebitoCartao debitoCartao;
public Payment(CartaoCredito cartaoCredito, BoletoBancario boletoBancario, DebitoCartao debitoCartao) {
this.cartaoCredito = cartaoCredito;
this.boletoBancario = boletoBancario;
this.debitoCartao = debitoCartao;
}
public void doPayment() {
// FAz o pagamento
}
}
Do jeito que estamos fazendo, estamos usando injeção de dependência, isso é legal, mas fora isso, segundo o principio DIP, nossa classe está ERRADA DEMAIS, pois note, que a classe de alto nível Payment está dependendo de outras 3 classes concretas, CartaoCredito, BOletoBancario e DebitoCartao, lembra? não podemos depender de implementações e sim de abstrações...
Portanto, vamos criar uma interface chamada PaymentInterface
public interface PaymentInterface {
void doSomething(double valor);
}
E agora vamos fazer com que as 3 classes de meios de pagamento implementem essa interface.
public class DebitoCartao implements PaymentInterface{
@Override
public void doSomething(double valor) {
System.out.println("Pagamento feito no débito, valor: " + valor);
}
}
e faremos essa implementação para todos os outros.
Agora na nossa classe Payment, vamos refatorá-la para atender ao DIP.
public class Payment {
private PaymentInterface meioPagamento;
public Payment(PaymentInterface meioPagamento) {
this.meioPagamento = meioPagamento;
}
public void doPayment(double valor) {
meioPagamento.doSomething(valor);
}
}
Notou que agora estamos dependendo de uma Interface ao invés de uma implementação concreta? dessa forma conseguiremos fazer nossos pagamentos sem nos importar com a implementação de cada Classe de baixo nível (Boleto, Crédito e Débito)
vamos fazer o teste usando uma classe que instancia Payment e faz o fluxo:
public class Program {
public static void main(String[] args) {
PaymentInterface debito = new DebitoCartao();
Payment pagamento = new Payment(debito);
pagamento.doPayment(1200);
}
}
Agora no Crédito, usando a mesma lógica:
public class Program {
public static void main(String[] args) {
PaymentInterface credito = new CartaoCredito();
Payment pagamento = new Payment(credito);
pagamento.doPayment(1200);
}
}
Lembre-se, dependa sempre de abstrações(interface) e não de implementações (classes concretas)
Até a próxima :)
Top comments (0)