Era SOLID o que me faltava

Clinton Rocha - Jan 2 - - Dev Community

Começou agora na programação orientada a objetos e não sabe sobre SOLID? Não se preocupe, nesse artigo vou te explicar e dar exemplos de como usá-lo no desenvolvimento do seu código.

O que é SOLID?

Na programação orientada a objetos, o termo SOLID é um acrônimo para cinco postulados de design, destinados a facilitar a compreensão, o desenvolvimento e a manutenção de software.

Ao usar esse conjunto de princípios é notável a redução na produção de bugs, melhora na qualidade do código, produção de códigos mais organizados, na redução de acoplamento, na melhora da refatoração e estimula o reaproveitamento do código.

S - Princípio da responsabilidade única

exemplo Princípio da responsabilidade única

SRP - Single Responsibility Principle

Esse principio diz que uma classe deve ter um, e somente um, motivo para mudar

É isso ai, nada de fazer classes que tenha varias funcionalidades e responsabilidades. Provavelmente você já fez ou se deparou com alguma classe que faz de tudo um pouco, a tal da God Class. Pode parecer que está tudo bem naquele momento, porem quando for necessário fazer alguma alteração na logica dessa classe é certeza que os problemas vão começar a aparecer.

God Class - Classe Deus: Na programação orientada a objetos, é uma classe que sabe demais ou faz demais.

class Task {
    createTask(){/*...*/}
    updateTask(){/*...*/}
    deleteTask(){/*...*/}

    showAllTasks(){/*...*/}

    existsTask(){/*...*/}

    TaskCompleter(){/*...*/}
}
Enter fullscreen mode Exit fullscreen mode

Essa classe Task esta quebrando o princípio do SRP por esta fazendo QUATRO tarefas diferentes. Ela está lidando com os dados, exibição, validação e verificação da Task.

Problemas que isso pode causar:

  • Falta de nexo - uma classe não deve assumir responsabilidades que não são suas;
  • Muita informação junta - sua classe vai ficar com muitas dependências e uma grande dificuldade para alterações;
  • Dificuldades na implementação de testes automatizados - é difícil de “mockar” esse tipo de classe;

Agora aplicando SRP na classe Task, vamos ver a melhora que esse principio pode causar:

class TaskHandler{ 
    createTask() {/*...*/} 
    updateTask() {/*...*/} 
    deleteTask() {/*...*/} 
} 

class TaskViewer{ 
    showAllTasks() {/*...*/} 
} 

class TaskChecker { 
    existsTask() {/*...*/} 
} 

class TaskCompleter { 
    completeTask() {/*...*/} 
}
Enter fullscreen mode Exit fullscreen mode

Daria para colocar create, update e delete em classes separadas, mas ao depender do contexto e tamanho do projeto é bom evitar complexidade desnecessária.

Talvez você tenha se perguntado só vou conseguir aplicar isso em classes? não, pelo contrário, da para aplicar em métodos e funções também.

//❌
function emailClients(clients: IClient[]) {

    clients.forEach((client)=>{
        const clientRecord = db.find(client);

        if(clientRecord){
            sendEmail(client);
        }
    })
}

//✅
function isClientActive(client: IClient):boolean { 
    const clientRecord = db.find(client); 
    return !!clientRecord; 
}

function getActiveClients(clients: IClient[]):<IClient | undefined> { 
    return clients.filter(isClientActive); 
}

function emailClients(clients: IClient[]):void { 
    const activeClients = getActiveClients(clients);
    activeClients?.forEach(sandEmail); 
}
Enter fullscreen mode Exit fullscreen mode

Código mais bonito, elegante e organizado. Esse principio é a base para os outros, ao conseguir aplicá-lo você estará fazendo um código de ótima qualidade, de fácil leitura e de fácil manutenção.

O - Princípio Aberto Fechado

exemplo Princípio Aberto-Fechado

OCP - Open-Closed Principle

Esse principio diz que Objetos ou entidades devem estar abertos para extensão, mas fechados para modificação, se for necessário adicionar uma funcionalidade é melhor estender e não alterar seu código fonte.

Imagine um pequeno sistema para secretaria de escolar, nele existem duas classes que representa a grade de aulas dos alunos, ensino fundamental e ensino médio. Além de uma classe que é para definir as aulas do aluno.

class EnsinoFundamental {
    gradeCurricularFundamental(){}
}

class EnsinoMedio {
    gradeCurricularMedio(){}
}

class SecretariaEscola {
    aulasDoAluno: string; 

    cadastrarAula(aulasAluno){
        if(aulasAluno instanceof EnsinoFundamental){
            this.aulasDoAluno = aulasAluno.gradeCurricularFundamental();
        } else if(aulasAluno.ensino instanceof EnsinoMedio){
            this.aulasDoAluno = aulasAluno.gradeCurricularMedio();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

A classe SecretariaEscola é responsável por verificar qual o ensino do aluno para conseguir aplicar a regra de negócio certa na hora do cadastrar as aulas. Agora imagine que essa escola tenha adicionado o ensino técnico e sua grade de aulas no sistema, vai ser necessário modificar essa classe, certo?, só que ai você esbarra em um problema, o de violar o Princípio Aberto-Fechado do SOLID.

Qual a solução que te vem a cabeça? Provavelmente adicionar um else if na classe e pronto, problema resolvido 😁. Não pequeno Padawan 😐, ai que está o problema!

Alterar uma classe já existente para adicionar um novo comportamento, corremos um sério risco de introduzir bugs em algo que já estava funcionando.

Lembre-se: OCP preza que uma classe deve estar fechada para alteração e aberta para extensão.

Veja a beleza que fica ao refatorar o código:

interface gradeCurricular {
    gradeDeAulas();
}

class EnsinoFundamental implements gradeCurricular {
    gradeDeAulas(){}
}

class EnsinoMedio implements gradeCurricular {
    gradeDeAulas(){}
}

class EnsinoTecnico implements gradeCurricular {
    gradeDeAulas(){}
}

class SecretariaEscola {
    aulasDoAluno: string;

    cadastrarAula(aulasAluno: gradeCurricular) {
        this.aulasDoAluno = aulasAluno.gradeDeAulas();
    }
}
Enter fullscreen mode Exit fullscreen mode

Veja a classe SecretariaEscola ela não precisa mais saber quais os métodos chamar para cadastrar a aula. Ela será capaz de cadastrar a grade de aulas corretamente de qualquer novo tipo de modalidade de ensino que seja criado, observe que adicionei o EnsinoTecnico sem nenhuma necessidade de mudar o código-fonte.

Desde que implemente a interface gradeCurricular.

Separe o comportamento extensível por trás de uma interface e inverta as dependências.

Uncle Bob

  • Aberto para extensão: você pode adicionar alguma nova funcionalidade ou comportamento a classe sem alterar seu código-fonte.
  • Fechado para modificação: se sua classe já tem uma funcionalidade ou comportamento que não apresenta problema algum, não altere o código-fonte dela para colocar algo novo.

L - Princípio da substituição de Liskov

exemplo Princípio da substituição de Liskov

LSP - Liskov Substitution Principle

Princípio da substituição de Liskov — Uma classe derivada deve ser substituível por sua classe base.

Esse principio que o mano Liskov introduziu em uma conferência no ano de 1987 é um pouco complicada de se entender ao ler a explicação dele, mas não se preocupe, vou mostrar outra explicação e um exemplo que vai te ajudar a entender.

Se para cada objeto o1 do tipo S há um objeto o2 do tipo T de forma que, para todos os programas P definidos em termos de T, o comportamento de P é inalterado quando o1 é substituído por o2 então S é um subtipo de T

Entendeu? Não né, eu também não entendi na primeira vez que li isso (nem nas outras dez vezes), mas calma aí, existe outra explicação:

Se S é um subtipo de T, então os objetos do tipo T, em um programa, podem ser substituídos pelos objetos de tipo S sem que seja necessário alterar as propriedades deste programa. — Wikipedia.

Se você é mais visual calma que tenho exemplos em código:

class Fulano {
    falarNome() {
        return "sou fulano!";
    }
}

class Sicrano extends Fulano {
    falarNome() {
        return "sou sicrano!";
    }
}

const a = new Fulano();
const b = new Sicrano();

function imprimirNome(msg: string) {
    console.log(msg);
}

imprimirNome(a.falarNome()); // sou fulano!
imprimirNome(b.falarNome()); // sou sicrano!

Enter fullscreen mode Exit fullscreen mode

A classe pai e a classe derivada estão passando como parâmetro e o código continua funcionando da forma esperada, magica? Que nada, é o princípio do nosso mano Liskov.

Exemplos de violações:

  • Sobrescrever/implementar um método que não faz nada;
  • Lançar uma exceção inesperada;
  • Retornar valores de tipos diferentes da classe base;

I - Princípio da Segregação da Interface

exemplo Princípio da Segregação da Interface<br>

ISP - Interface Segregation Principle

Princípio da Segregação da Interface — Uma classe não deve ser forçada a implementar interfaces e métodos que não irão utilizar.

Esse principio diz que é melhor criar interfaces mais especificas do que uma interface genérica.

No exemplo a seguir foi criado uma interface Animal para abstrair os comportamentos de animais e em seguida as classes implementam essa interface, veja:

interface Animal {
    comer();
    dormir();
    voar();
}

class Pato implements Animal{
    comer(){/*faz algo*/};
    dormir(){/*faz algo*/};
    voar(){/*faz algo*/};
}

class Peixe implements Animal{
    comer(){/*faz algo*/};
    dormir(){/*faz algo*/};

    voar(){/*faz algo*/};
    // Esta implementação não faz sentido para um peixe 
    // ela viola o Princípio da Segregação da Interface
}
Enter fullscreen mode Exit fullscreen mode

A interface genérica Animal esta forçando a classe Peixe a ter um comportamento que faz sentido e acaba viola o principio ISP e o LSP também.

Resolvendo esse problema usando o ISP:

interface Animal {
    comer();
    dormir();
}

interface AnimalQueVoa extends Animal {
    voar();
}

class Peixe implements Animal{
    comer(){/*faz algo*/};
    dormir(){/*faz algo*/};
}

class Pato implements AnimalQueVoa {
    comer(){/*faz algo*/};
    dormir(){/*faz algo*/};
    voar(){/*faz algo*/};
}
Enter fullscreen mode Exit fullscreen mode

Agora ficou melhor, foi retirado o método voar() da interface Animal e adicionamos em uma interface derivada AnimalQueVoa. Com isso o comportamento foi isolado de maneira correta dentro do nosso contexto e ainda respeitamos o principio de segregação das interfaces.

D - Princípio da inversão da dependência

exemplo Princípio da inversão da dependência

DIP — Dependency Inversion Principle

Princípio da Inversão de Dependência — Dependa de abstrações e não de implementações.

  1. Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender da abstração.

  2. Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.

  • Uncle Bob

No exemplo a seguir vou mostrar um codigo simples para ilustrar o DIP. Nesse exemplo temos um sistema de notificação que envia mensagens por diferentes meios, como e-mail e SMS. Primeiro vamos criar classes concretas para esses meios de notificação:

class EmailNotification {
  send(message) {
    console.log(`Enviando e-mail: ${message}`);
  }
}

class SMSNotification {
  send(message) {
    console.log(`Enviando SMS: ${message}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Agora, vamos criar uma classe de serviço que depende dessas implementações concretas:

class NotificationService {
  constructor() {
    this.emailNotification = new EmailNotification();
    this.smsNotification = new SMSNotification();
  }

  sendNotifications(message) {
    this.emailNotification.send(message);
    this.smsNotification.send(message);
  }
}
Enter fullscreen mode Exit fullscreen mode

No exemplo acima, NotificationService depende diretamente das implementações concretas de EmailNotification e SMSNotification. Isso viola o DIP, pois a classe de alto nível NotificationService está diretamente dependente de classes de baixo nível.

Vamos corrigir esse código usando DIP. Em vez de depender de implementações concretas, a classe de alto nível NotificationService deve depender de abstrações. Vamos criar uma interface Notification como abstração:

// Abstração para o envio de notificações
interface Notification { 
    send(message: string): void
}
Enter fullscreen mode Exit fullscreen mode

Agora, as implementações concretas EmailNotification e SMSNotification devem implementar essa interface:

class EmailNotification implements Notification {
  send(message: string) {
    console.log(`Enviando e-mail: ${message}`);
  }
}

class SMSNotification implements Notification {
  send(message: string) {
    console.log(`Enviando SMS: ${message}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Finalmente, a classe de serviço de notificação pode depender da abstração Notification:

class NotificationService {

private notificationMethod: Notification;

  constructor(notificationMethod: Notification) {
    this.notificationMethod = notificationMethod;
  }

  sendNotification(message: string) {
    this.notificationMethod.send(message);
  }
}
Enter fullscreen mode Exit fullscreen mode

Dessa forma, a classe de serviço NotificationService depende de uma abstração Notification, e não das implementações concretas, cumprindo assim o Princípio da Inversão de Dependência.

Conclusão

Ao adotar esses princípios, os desenvolvedores podem criar sistemas mais resilientes às mudanças, facilitando a manutenção e melhorando a qualidade do código ao longo do tempo.

Todo esse conteúdo foi baseado em anotações, outros artigos e vídeos que encontrei pela internet durante meu estudo sobre POO, as explicações são próximas aos autores dos princípios, já os códigos usados nos exemplos eu criei baseado no meu entendimento dos princípios. Espero ter ajudado a você leitor na progressão dos seus estudos.

. . . . .