Introdução ao SOLID - Princípios S O D
Gabriel_Silvestre
Posted on May 20, 2022
Tabela de Conteúdo
- SOLID
- Single Responsibility Principle
- Open/Closed Principle
- Dependency Inversion Principle
- Repositório
- Links Úteis
SOLID
O que é?
SOLID, na programação, refere-se a cinco princípios que focam na escrita de um código limpo, legível e de fácil manutenção.
Princípios
- Single responsibility principle
- Open/Close principle
- Liskov substitution principle
- Interface segregation principle
- Dependency inversion principle
Aviso
O SOLID propõe PRINCÍPIOS a serem seguidos para o desenvolvimento de um código mais limpo, porém esses princípios não são absolutos e podem muito bem ser ignorados ou adaptados ao nosso contexto.
Em outras palavras, o SOLID nos da boas recomendações a serem seguidas, mas não passa disso, RECOMENDAÇÕES.
"Regra nenhuma, princípio nenhum e caso especial nenhum deve piorar a legibilidade ou a manutenibilidade do seu código."
Single Responsibility Principle
Recomendação
O princípio da responsabilidade única recomenda que cada função ou classe realize uma única ação, dessa forma teremos nosso código divido em pequenos blocos altamente "especializados".
Exemplo
Imagine que precisemos efetuar o pagamento de uma conta em outra moeda, para isso vamos precisar converter o valor para Real, acessar nossa carteira (banco de dados), recuperarmos nosso saldo e se nosso saldo for suficiente, pagar a conta.
const EXCHANGE = {
USD: 4.6,
EUR: 5,
CAD: 3.7,
};
type ICurrency = keyof typeof EXCHANGE;
const convertValue = (value: number, currency: ICurrency) => {
return Math.floor(value * EXCHANGE[currency]);
};
const getBalance = async () => {
const query = `
SELECT amount FROM user.wallet;
`;
const [{ amount }] = await mysql.execute(query);
return amount;
};
const isEnough = (personalBalance: number, totalAmount: number) => {
return personalBalance > totalAmount;
};
const payBill = async (billValue: number, billCurrency: ICurrency) => {
const myBalance = await getBalance();
const convertedValue = convertValue(billValue, billCurrency);
if (isEnough(myBalance, convertedValue)) {
return `Conta paga | saldo anterior ${myBalance} | saldo atual ${
myBalance - convertedValue
}`;
}
return `Saldo insuficiente | saldo anterior ${myBalance} | saldo atual ${myBalance}`;
};
No exemplo acima, apesar de termos omitido alguns passos, conseguimos criar um código desacoplado, legível e de fácil entendimento. Isso foi possível graças à alta especialização do código, onde cada função é responsável por executar uma única ação.
Open/Closed Principle
Recomendação
O princípio aberto/fechado diz que o comportamento de uma entidade deve ser aberto a extensões, mas fechado a modificações, em palavras mais simples, precisamos poder criar novos comportamentos sem alterar os já existentes.
Exemplo
Imagine que uma faculdade nos pediu para desenvolvermos um software que irá validar se os candidatos conseguiram passar no vestibular para determinado curso.
Para isso nós vamos precisar saber a nota do candidato, a nota de corte do curso e quantas vagas o curso possui, ou seja, temos duas regras variáveis: nota de corte e quantidade de vagas.
type Student = {
name: string;
examNote: number;
course: Course;
}
type Course = {
name: string;
openings: number;
minNote: number;
}
const students = [
{
name: 'John',
examNote: 87,
course: {
name: 'ADM',
openings: 40,
minNote: 62,
},
},
{
name: 'Joe',
examNote: 42,
course: {
name: 'ENG',
openings: 20,
minNote: 76,
},
},
{
name: 'Katy',
examNote: 66,
course: {
name: 'MED',
openings: 10,
minNote: 85,
},
},
];
const rankingStudents = (students: Student[]) => {
return students.sort((a, b) => a.examNote - b.examNote );
};
const hasOpening = (listLength: number, listLimit: number) => {
return listLength < listLimit;
};
const hasNote = (studentNote: number, minNote: number) => {
return studentNote >= minNote;
}
const approvedList = {
ADM: [],
ENG: [],
MED: [],
}
const approvedStudents = (students: Student[]) => {
return rankingStudents(students)
.reduce((acc, { name, examNote, course }) => {
const courseApprovedList = acc[course.name];
if (!hasOpening(courseApprovedList.length, course.openings)) return acc;
if (hasNote(examNote, course.minNote)) {
acc[couse.name].push(name);
}
return acc;
}, approvedList);
};
No exemplo construído logo acima, estamos montando a lista de aprovados de acordo com o número de vagas e a nota de corte de cada curso, sendo que ambos os dados são variáveis, ou seja, podemos adicionar qualquer curso que quisermos a esse software sem a necessidade de alterá-lo.
Dependency Inversion Principle
Recomendação
Antes de entrar mais a fundo nas recomendações, vale a citação que o princípio de inversão de dependências anda em paralelo com a arquitetura de Injeção de Dependências, essa que não será abordada aqui.
O princípio de inversão de dependências consiste na implementação de uma dependência baseada em uma abstração, normalmente uma Interface, ao invés de um objeto, valor ou classe propriamente dito.
Exemplo
Diferente dos exemplos anteriores, aqui iremos utilizar da Orientação a Objeto para exemplificar, pois acredito que seja mais fácil de entender dessa forma.
Pense no seguinte cenário, temos uma pessoa e ela tem vários jogos para jogar, porém todos esses jogos são de RPG, logo suas ações são bem similares. O objetivo é criar uma classe Pessoa que possa jogar qualquer jogo de RPG.
export interface IGameRPG {
fight(): number; // <- retorna o dano causado
flee(): boolean; // <- retorna se conseguiu fugir ou não
levelUp(): void;
get level(): number; // <- nível do jogo
}
class Gamer {
constructor(private gameRpg: IGameRPG) {}
combat() {
return this.gameRpg.fight();
}
escape() {
return this.gameRpg.flee();
}
improve() {
this.gameRpg.levelUp();
}
}
class LightSouls implements IGameRPG {
private _level: number = 1;
// 10 à 100 de dano
fight() {
console.log('Bate, rola, rola, bate, potion, bate');
return Math.floor(Math.random() * (100 - 10 + 1)) + 10;
}
// 30% de chance de fugir
flee() {
console.log('Correeeeee');
return Math.random() < 0.3;
}
levelUp() {
console.log("Welcome Home, ashen one. Speak thine heart's desire.");
this._level += 1;
}
get level() {
return this._level;
}
}
export class MoOnline implements IGameRPG {
private _level: number = 1;
// 1000 à 10000 de dano
fight() {
console.log('Bate, bate, bate, bate, bate, potion, bate, bate');
return Math.floor(Math.random() * (10000 - 1000 + 1)) + 1000;
}
// 60% de chance de fugir
flee() {
console.log('Sair da missão');
return Math.random() < 0.6;
}
levelUp() {
console.log('Acessando novo andar...');
this._level += 1;
}
get level() {
return this._level;
}
}
const moOnline = new MoOnline();
const lightSouls = new LightSouls();
const moGamer = new Gamer(moOnline); // <- a classe Gamer recebe o jogo Mo Online
const soulsGamer = new Gamer(lightSouls); // <- a classe Gamer recebe o jogo Light Souls
moGamer.combat(); // Bate, bate, bate, bate, bate, potion, bate, bate
soulsGamer.combat(); // Bate, rola, rola, bate, potion, bate
Como podemos ver no exemplo acima, a classe Gamer pode receber qualquer jogo, desde que ele seja uma implementação da abstração IGameRPG
.
Isso só é possível, pois estamos garantindo, através da interface IGameRPG
, que todo a classe que a implemente vai ter os métodos fight
, flee
e levelUp
.
Repositório
Todos os exemplos demonstrados aqui se encontram nesse repositório, fique a vontade para favoritar, clonar ou realizar um fork.
Este repositório está em construção. Novos exemplos serão adicionados em breve.
Links Úteis
Posted on May 20, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.