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?
- S - Princípio da responsabilidade única
- O - Princípio Aberto-Fechado
- L - Princípio da substituição de Liskov
- I - Princípio da Segregação da Interface
- D - Princípio da inversão da dependência
- Conclusão
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
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(){/*...*/}
}
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() {/*...*/}
}
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);
}
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
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();
}
}
}
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();
}
}
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
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!
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
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
}
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*/};
}
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
DIP — Dependency Inversion Principle
Princípio da Inversão de Dependência — Dependa de abstrações e não de implementações.
Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender da abstração.
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}`);
}
}
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);
}
}
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
}
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}`);
}
}
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);
}
}
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.
Top comments (13)
Cara valeu pelo conteúdo, sempre busco ver novas visões sobre assuntos que aprendi no curso ou por contra própria e ter contato com um texto tão bem redigido, com exemplos tantos práticos quanto visuais tão bem feitos e selecionados é incrível. Muito obrigado por compartilhar seu conhecimento 🦤.
Ps: É mais como uma recomendação que pode impulsionar seus posts, tenta utilizar a tag #braziliandevs, facilita de ser encontrado por outros usuários que não se sentem tão confortável em ler inglês.
Por que eu tive sorte de me deparar com esse post KKKKKKKObrigado pela mensagem e dica da hashtag, fico feliz que tenha te ajudado. 😁
Artigo brabissimo, sempre bom reler POO
Thanks for sharing
Arrasou demais na explicação primo 🤌
Ótimo artigo, o título foi nota 10!
Os exemplos (por si so já é totalmente auto explicativa), e a dinamica da leitura foi massa demais primo. Muito bom o artigo, esperando ansiosamente por mais.
qualidade absurda de conteudo! eu tenho minhas ressalvas com SOLID, mas é muito bom aprender denovo com uma linguagem amigavel
Ótima explicação!
Consegui compreender bem os conceitos.
Ficou bem boa as ilustrações do conteúdo primo. Pra mim, elas sozinha já fizeram total sentido sem ler muito o conteúdo do texto.
Um dia chego nesse nível de imagens auto-explicativas!!!
Great article!! 🤟🏻