Article image
Caio Oliveira
Caio Oliveira06/05/2023 11:36
Compartilhe

S.O.L.I.D

  • #Java
  • #Design Thinking

Atualmente, um assunto que é muito discutido entre pessoas no ramo de desenvolvimento é a ideia de S.O.L.I.D, mas afinal de contas... Que diabo é isso?

O S.O.L.I.D nada mais é queum conjunto de cinco princípios de design de software que nos ajuda na criação de sistemas mais robustos, flexíveis e fáceis de manter. Cada princípio do SOLID é um acrônimo que representa uma regra específica de orientação para o desenvolvimento de software orientado a objetos.

Os cinco princípios do SOLID

S -> Single Responsibility Principle ( Princípio da Responsabilidade Única)

O -> Open-Closed Principle ( Princípio aberto-fechado)

L -> Liskov Substitution Principle (Princípio de Substituição de Liskov)

I -> Interface Segregation Principle (Princípio de Segregação de Interface)

D -> Dependency Inversion Principle (Princípio da Segregação de Dependencia)

A partir de agora irei detalhar sobre cada um deles, com exemplos de como corrigir tais violações! Usarei o Java nos para ilustrar cada uma dessas situações.

Princípio da Responsabilidade Única

Esse princípio afirma que uma classe deve ter apenas uma responsabilidade. Isso significa que uma classe deve ter apenas uma tarefa ou função a desempenhar.

Considere o seguinte exemplo:

public class Funcionario {
private String nome;
private String cargo;
private double salarioBase;

public Funcionario(String nome, String cargo, double salarioBase) {
  this.nome = nome;
  this.cargo = cargo;
  this.salarioBase = salarioBase;
}

public double calculaSlario() {
  double salario = 0.0;
  //lógica de cálculo de salário com base nas informações do funcionário
  return salario;
}
//getters and setters
}

Note que a classe "Funcionario" que não apenas armazena informações do funcionário, mas também calcula seu salário com base em regras de negócios. Isso significa que a classe tem mais de uma responsabilidade: armazenar informações e calcular o salário.

Para consertar essa violação, podemos dividir a classe "Funcionario" em duas classes: uma classe "Funcionario" que armazena informações do funcionário e outra classe "CalculadoraDeSalario" que calcula o salário com base nas informações do funcionário.

public class Funcionario {
private String nome;
private String cargo;
private double salarioBase;

public Funcionario(String nome, String cargo, double salarioBase) {
  this.nome = nome;
  this.cargo = cargo;
  this.salarioBase = salarioBase;
}

//getters and setters
}
public class CalculadoraDeSalario {
public double calcula(Funcionario funcionario) {
  double salario = 0.0;
  //lógica de cálculo de salário com base nas informações do funcionário
  return salario;
}
}

Desta forma, cada classe terá apenas uma responsabilidade e será mais fácil de manter, testar e entender.

Princípio Aberto-Fechado

Tal princípio afirma que uma classe deve estar aberta para extensão, mas fechada para modificação. Isso significa que o comportamento de uma classe deve ser estendido por meio de herança ou composição, mas o código-fonte da classe original não deve ser alterado.

Imagine a seguinte classe de Processamento de pagamento

public class PaymentProcessor {
public void processPayment(Payment payment) {
  if (payment.getType() == PaymentType.CREDIT_CARD) {
  	System.out.println("Processamento de Cartão de Crédito realizado!")
  } else if (payment.getType() == PaymentType.BOLETO) {
  	System.out.println("Processamento de Boleto realizado!")
  }
}
}

Qual o problema dessa classe? 

O que acontece se eu precisar criar um novo tipo de pagamento? Inevitávelmente será preciso alterar essa classe para o funcionamento adequado, o que violaria o princípio Aberto-Fechado.

Uma das soluções para esse problema é o padrão Strategy. Para isso implementaremos uma interface chamada PaymentStrategy:

public interface PaymentStrategy {
void processPayment(Payment payment);
}

Agora, iremos criar classes a partir da nossa interface que implementem cada uma das formas de pagamento.

public class CreditCardPaymentStrategy implements PaymentStrategy {
public void processPayment(Payment payment) {
  System.out.println("Processamento de Cartão de Crédito realizado!")
}
}
public class BoletoPaymentStrategy implements PaymentStrategy {
public void processPayment(Payment payment) {
  System.out.println("Processamento de Boleto realizado!")
}
}

Agora, podemos criar a classe Payments processor, de modo que ela aceite qualquer uma das formas de pagamentos

public class PaymentProcessor {
private final PaymentStrategy paymentStrategy;

public PaymentProcessor(PaymentStrategy paymentStrategy) {
  this.paymentStrategy = paymentStrategy;
}

public void processPayment(Payment payment) {
  paymentStrategy.processPayment(payment);
}
}

Note que dessa forma, para adicionar uma nova forma de pagamento, não será necessário alterar a classe de processamento, basta criar uma nova classe extendendo a nossa estratégia.

Princípio da Substituição de Liskov

Este princípio afirma que uma classe derivada deve ser substituível por sua classe base sem quebrar o comportamento do programa. Isso significa que as subclasses devem ser substituíveis pelas classes base em todas as situações.

Considere a seguinte situação:

public class Rectangle {
private int width;
private int height;

public Rectangle(int width, int height) {
  this.width = width;
  this.height = height;
}

public int getWidth() {
  return width;
}

public void setWidth(int width) {
  this.width = width;
}

public int getHeight() {
  return height;
}

public void setHeight(int height) {
  this.height = height;
}

public int getArea() {
  return width * height;
}
}
public class Square extends Rectangle {
public Square(int sideLength) {
  super(sideLength, sideLength);
}

@Override
public void setWidth(int width) {
  super.setWidth(width);
  super.setHeight(width);
}

@Override
public void setHeight(int height) {
  super.setHeight(height);
  super.setWidth(height);
}
}

Nesse exemplo, a classe Square estende a classe Rectangle, mas ao sobrescrever os métodos setWidth e setHeight para manter a altura e a largura sempre iguais isso faz com que a Classe Square não pode ser substituída pela classe Rectangle sem alterar o comportamento esperado. 

Para corrigir nosso código , devemos realizar a seguinte implementação:

public interface Shape {
int getArea();
}

Definindo a interface Shape, podemos criar as classes Rectangle e Square implementando Shape

public class Rectangle implements Shape {
private int width;
private int height;

public Rectangle(int width, int height) {
  this.width = width;
  this.height = height;
}

public int getWidth() {
  return width;
}

public void setWidth(int width) {
  this.width = width;
}

public int getHeight() {
  return height;
}

public void setHeight(int height) {
  this.height = height;
}

@Override
public int getArea() {
  return width * height;
}
}
public class Square implements Shape {
private int sideLength;

public Square(int sideLength) {
  this.sideLength = sideLength;
}

public int getSideLength() {
  return sideLength;
}

public void setSideLength(int sideLength) {
  this.sideLength = sideLength;
}

@Override
public int getArea() {
  return sideLength * sideLength;
}
}

Isso garante que cada forma tenha seus próprios comportamentos e propriedades e que a substituição de objetos de uma forma por outra não afete a corretude do programa, respeitando o princípio LSP.

Princípio da Segregação de Interface

Esse princípio nos diz que uma classe não deve ser forçada a implementar interfaces que não usa. Em vez disso, as interfaces devem ser segregadas em grupos menores de funcionalidades relacionadas e as classes devem implementar apenas as interfaces relevantes.

Suponha que desejamos criar objetos para os seguintes animais: Pato e Cão, utilizando a seguinte interface

public interface Animal {
void voar();
}

 

Evidentemente, Cão não voa (Pelo menos até onde eu sei), portanto implementar tal classe em nosso amiguinho canino é uma violação direta do principio da segregação de interface. Para corrigir, devemos criar uma interface para cada tipo de animal!

public interface Voador {
void voar();
}
public interface Andador {
void andar();
}
public class Pato implements Voador, Andador {
@Override
public void voar() {
  System.out.println("Pato voando, meio capenga, mas voando");
}

@Override
public void andar() {
  System.out.println("Pato andando");
}
}
public class Cao implements Andador {
@Override
public void andar() {
  System.out.println("Cão andando");
}
}

Desta forma, conseguimos segregar nossa interface Animal em outras duas, de modo que sempre que sejam implementadas, seus métodos são usados efetivamente.

Princípio da Inversão de Dependência:

Este é o ultimo princípio, o qual afirma que os módulos de alto nível não devem depender dos módulos de baixo nível, mas ambos devem depender de abstrações. Isso significa que as dependências devem ser invertidas, de modo que as classes de nível superior dependam de interfaces abstratas, em vez de implementações concretas de classes de nível inferior.

Considere o exemplo abaixo:

public class OrderService {
private OrderRepository orderRepository;

public OrderService() {
  this.orderRepository = new OrderRepository();
}

public List<Order> getAllOrders() {
  return orderRepository.getAllOrders();
}

// outros métodos
}
public class OrderRepository {
public List<Order> getAllOrders() {
  // obtém todas as ordens
}

// outros métodos
}

A Classe OrderService é um módulo de alto nivel e depende diretamente da classe concreta OrderRepository. Isso é uma violação do principio de inversão de dependencia. Desejamos que classe de alto nivel sejam independentes de abstrações concretas para gerar desacoplamento entre elas, podemos corrigir o código acima da seguinte maneira

public class OrderService {
private OrderRepositoryInterface orderRepository;

public OrderService() {
  this.orderRepository = new OrderRepository();
}

public List<Order> getAllOrders() {
  return orderRepository.getAllOrders();
}

// outros métodos
}
public interface OrderRepositoryInterface {
public List<Order> getAllOrders();

// outros métodos
}
public class OrderRepository implements OrderRepositoryInterface{
public List<Order> getAllOrders() {
  // obtém todas as ordens
}

}

Deste forma, podemos mudar a implementação da classe OrderRepository sempre que necessário, sem comprometer o funcionamento da classe OrderService!

Espero ter ajudado com essa breve explicação. That's All Folks

Compartilhe
Comentários (3)
Pamela Canale
Pamela Canale - 17/06/2024 12:16

Parabéns muito bom artigo, simples e fácil de entender!!

JC

John Cordeiro - 06/05/2023 13:54

Parabéns, muito bom e explicativo. Clareou os princípios e objetivos!

Leonardo Santos
Leonardo Santos - 06/05/2023 22:51

Muito parabéns