Article image
Edson Araújo
Edson Araújo06/05/2024 15:54
Compartilhe

SOLID - A maneira simples de entender

  • #PHP
  • #TypeScript
  • #Java

Olá!!! Como você tem estado? Você está bem? Espero que sim!

Hoje vou falar de um tema que todo mundo fala ou escreve. Mas às vezes é difícil compreender todos os princípios. Estou falando do SÓLIDO.

Muitas pessoas, quando pergunto sobre o SOLID, provavelmente sempre se lembram do primeiro princípio (Princípio da Responsabilidade Única). Mas quando pergunto sobre outro, algumas pessoas não lembram ou têm dificuldade de explicar. E eu entendo.

Na verdade, é difícil explicar sem codificar ou relembrar a definição de cada princípio. Mas neste artigo quero apresentar cada princípio de maneira fácil. Então usarei o Typescript para exemplificar.

Então vamos começar!

Single Responsability Principle - SRP

(Princípio de Responsabilidade Única)

O princípio mais fácil de entender e lembrar.

Quando estamos codificando, é fácil identificar quando estamos esquecendo o princípio.

Vamos imaginar que temos uma classe TaskManager:

class TaskManager {
constructor() {}
connectAPI(): void {}
createTask(): void {
  console.log("Create Task");
}
updateTask(): void {
  console.log("Update Task");
}
removeTask(): void {
  console.log("Remove Task");
}
sendNotification(): void {
  console.log("Send Notification");
}
sendReport(): void {
  console.log("Send Report");
}
}

Tudo bem! Provavelmente você percebe seu problema, não é?

A classe TaskManager tem muitas responsabilidades que não pertencem a ela. Por exemplo: métodos sendNotification e sendReport.

Agora, vamos refatorar e aplicar a solução:

class APIConnector {
constructor() {}
connectAPI(): void {}
}

class Report {
constructor() {}
sendReport(): void {
  console.log("Send Report");
}
}

class Notificator {
constructor() {}
sendNotification(): void {
  console.log("Send Notification");
}
}

class TaskManager {
constructor() {}
createTask(): void {
  console.log("Create Task");
}
updateTask(): void {
  console.log("Update Task");
}
removeTask(): void {
  console.log("Remove Task");
}
}

Simples, não é? Apenas separamos a notificação e o relatório em classes específicas. Agora estamos respeitando o Princípio Único de Responsabilidade!

A definição: Cada classe deve ter um, e apenas um, motivo para mudar

Open Closed Principle - OCP

(Princípio Aberto Fechado)

O segundo princípio. Além disso, considero fácil de entender. Uma dica para você, se você perceber que tem muitas condições em algum método para verificar algo, talvez seja no caso do OCP.

Vamos imaginar o seguinte exemplo de Classe Exame:

type ExamType = {
type: "BLOOD" | "XRay";
};

class ExamApprove {
constructor() {}
approveRequestExam(exam: ExamType): void {
  if (exam.type === "BLOOD") {
    if (this.verifyConditionsBlood(exam)) {
      console.log("Blood Exam Approved");
    }
  } else if (exam.type === "XRay") {
    if (this.verifyConditionsXRay(exam)) {
      console.log("XRay Exam Approved!");
    }
  }
}

verifyConditionsBlood(exam: ExamType): boolean {
  return true;
}
verifyConditionsXRay(exam: ExamType): boolean {
  return false;
}
}

Sim, provavelmente você já viu esse código várias vezes. Primeiro, estamos quebrando o primeiro princípio do SRP e estabelecendo uma série de condições.

Agora imagine se aparecer outro tipo de exame, por exemplo, o ultrassom. Precisamos adicionar outro método para verificar e outra condição.

Vamos refatorar este código:

type ExamType = {
type: "BLOOD" | "XRay";
};

interface ExamApprove {
approveRequestExam(exam: NewExamType): void;
verifyConditionExam(exam: NewExamType): boolean;
}

class BloodExamApprove implements ExamApprove {
approveRequestExam(exam: ExamApprove): void {
  if (this.verifyConditionExam(exam)) {
    console.log("Blood Exam Approved");
  }
}
verifyConditionExam(exam: ExamApprove): boolean {
  return true;
}
}

class RayXExamApprove implements ExamApprove {
approveRequestExam(exam: ExamApprove): void {
  if (this.verifyConditionExam(exam)) {
    console.log("RayX Exam Approved");
  }
}
verifyConditionExam(exam: NewExamType): boolean {
  return true;
}
}

Uau, muito melhor! Agora se aparecer outro tipo de exame apenas implementamos a interface ExamApprove. E caso surja outro tipo de verificação para o exame, apenas atualizamos a interface.

Definição:Entidades de software (como classes e métodos) devem estar abertas para extensão, mas fechadas para modificação

Liskov Substitution Principle - LSP

(Princípio de Substituição de Liskov)

Um dos mais complicados de entender e examinar. Mas como eu disse, vou facilitar o seu entendimento.

Imagine que você tem uma universidade e dois tipos de alunos. Aluno e Pós-Graduando.

class Student {
constructor(public name: string) {}

study(): void {
  console.log(`${this.name} is studying`);
}

deliverTCC() {
  /** Problem: Post graduate Students don't delivery TCC */
}
}

class PostgraduateStudent extends Student {
study(): void {
  console.log(`${this.name} is studying and searching`);
}
}

Temos um problema aqui, estamos prorrogando o Aluno, mas o Aluno de Pós-Graduação não precisa entregar TCC. Ele apenas estuda e pesquisa.

Como podemos resolver esse problema? Simples! Vamos criar uma classe Aluno e separar o Aluno de Graduação e Pós Graduação:

class Student {
constructor(public name: string) {}

study(): void {
  console.log(`${this.name} is studying`);
}
}

class StudentGraduation extends Student {
study(): void {
  console.log(`${this.name} is studying`);
}

deliverTCC() {}
}

class StudentPosGraduation extends Student {
study(): void {
  console.log(`${this.name} is studying and searching`);
}
}

Agora temos uma maneira melhor de separar suas respectivas responsabilidades. O nome deste princípio pode ser assustador, mas o seu princípio é simples.

Definição: Classes derivadas (ou classes filhas) devem ser capazes de substituir suas classes base (ou classes pai)

Interface Segregation Principle - ISP

(Princípio de segregação de interface)

Para entender esse princípio, o truque é lembrar a definição. Uma classe não deve ser forçada a implementar métodos que não serão usados.

Então imagine que você tem uma classe que implementa uma interface que nunca será usada.

Vamos imaginar um cenário com um Vendedor e uma Recepcionista de alguma loja. Tanto o vendedor quanto o recepcionista recebem salário, mas apenas o vendedor recebe comissão.

Vejamos o problema:

interface Employee {
salary(): number;
generateCommission(): void;
}

class Seller implements Employee {
salary(): number {
  return 1000;
}
generateCommission(): void {
  console.log("Generating Commission");
}
}

class Receptionist implements Employee {
salary(): number {
  return 1000;
}
generateCommission(): void {
  /** Problem: Receptionist don't have commission  */
}
}

Ambos implementam a interface Employee, mas a recepcionista não tem comissão. Então somos obrigados a implementar um método que nunca será utilizado.

Então a solução:

interface Employee {
salary(): number;
}

interface Commissionable {
generateCommission(): void;
}

class Seller implements Employee, Commissionable {
salary(): number {
  return 1000;
}

generateCommission(): void {
  console.log("Generating Commission");
}
}

class Receptionist implements Employee {
salary(): number {
  return 1000;
}
}

Calma, fácil! Agora temos duas interfaces! A classe empregadora e a interface comissionável. Agora apenas o Seller irá implementar as duas interfaces onde terá a comissão. A recepcionista não implementa apenas o funcionário. Assim a Recepcionista não será obrigada a implementar o método que nunca será utilizado.

Definição: Uma classe não deve ser forçada a implementar interfaces e métodos que não serão utilizados.

Dependency Inversion Principle - DIP

(Princípio de Inversão de Dependência)

O último! Pelo nome você pode pensar que é difícil de lembrar! Mas provavelmente você já vê esse princípio todas as vezes.

Imagine que você tem uma classe Service que se integra com uma classe Repository que irá chamar o Banco de Dados, por exemplo um Postgress. Mas se a classe do repositório mudar e o banco de dados mudar para um MongoDB, por exemplo.

Vejamos o exemplo:

interface Order {
id: number;
name: string;
}

class OrderRepository {
constructor() {}
saveOrder(order: Order) {}
}

class OrderService {
private orderRepository: OrderRepository;

constructor() {
  this.orderRepository = new OrderRepository();
}

processOrder(order: Order) {
  this.orderRepository.saveOrder(order);
}
}

Notamos que o repositório da classe OrderService está diretamente acoplado à implementação concreta da classe OrderRepository.

Vamos refatorar este exemplo:

interface Order {
id: number;
name: string;
}

class OrderRepository {
constructor() {}
saveOrder(order: Order) {}
}

class OrderService {
private orderRepository: OrderRepository;

constructor(repository: OrderRepository) {
  this.orderRepository = repository;
}

processOrder(order: Order) {
  this.orderRepository.saveOrder(order);
}
}

Legal! Muito melhor! Agora recebemos o repositório como parâmetro no construtor para instanciar e usar. Agora dependemos da abstração e não precisamos saber qual repositório estamos usando.

Definição: dependem de abstrações em vez de implementações concretas

Para finalizar

Então, como você está se sentindo agora? Espero que com esses exemplos fáceis você possa lembrar e entender o que e por que usar esses princípios em seu código. Fica mais fácil de entender e dimensionar, além de você aplicar código limpo.

Espero que você tenha gostado!

Muito obrigado e fiquem bem sempre!

Compartilhe
Comentários (0)