Os cinco princípios do design orientado a objeto em linguagens OOP. Para iniciarmos, vamos entender o que quer dizer cada letra do SOLID:
- S = Princípio da Responsabilidade Única (Single Responsibility Principle)
- O = Princípio Aberto/Fechado (Open/Closed Principle);
- L = Princípio de Substituição de Liskov (Liskov’s Substitution Principle);
- I = Princípio da Segregação da Interface (Interface Segregation Principle);
- D = Princípio da Inversão de Dependência (Dependency Inversion Principle).
não é sobre “usar classe por usar classe”, é sobre organizar responsabilidades. Nasceu no mundo da orientação a objetos e pode ser aplicado em praticamente qualquer linguagem: PHP, JavaScript, TypeScript, Java, C#, Python, etc.
SRP Quem é responsável por isso?
OCP Como adicionar sem modificar?
DIP Como evitar ficar preso a uma implementação?solid é como não explorar um funcionario que faz tudo em uma empresa
imagine um cenário:
João
├── atende cliente
├── faz financeiro
├── programa
├── testa
├── implanta
├── dá suporte
├── vende
└── ainda limpa o escritórioFunciona por um tempo? Funciona.
Mas quando dá problema, ninguém sabe onde mexer. Quando precisa mudar algo, tudo depende do João. Quando o João erra, a empresa inteira para.
S – Single Responsibility Principle
uma classe deve ter apenas uma responsabilidade
console.warn(`________________________________ before SOLID ________________________________`)
class UsuarioService {
cadastrar(nome, email, senha) {
// validar dados
if (!nome || !email || !senha) {
throw new Error('Dados obrigatórios')
}
if (!email.includes('@')) {
throw new Error('Email inválido')
}
if (senha.length < 6) {
throw new Error('Senha muito curta')
}
// criptografar senha
const senhaCriptografada = 'hash_' + senha
// salvar no banco
console.log('Salvando usuário no banco:', {
nome,
email,
senha: senhaCriptografada,
})
// enviar email
console.log(`Enviando email de boas-vindas para ${email}`)
return {
nome,
email,
senha: senhaCriptografada,
}
}
}
const user = new UsuarioService()
const novoUser = user.cadastrar(
'Geraldo Costa Filho',
'contato@geraldo.io',
'123456'
)
console.log(`===================================================================================`)
console.warn(`________________________________ afterSRP Single Responsibility Principle ________________________________`)
/*
Uma classe deve ter apenas uma responsabilidade.
Vamos.
*/
class UsuarioValidator {
validar(nome, email, senha) {
if (!nome || !email || !senha) {
throw new Error('Dados obrigatórios')
}
if (!email.includes('@')) {
throw new Error('Email inválido')
}
if (senha.length < 6) {
throw new Error('Senha muito curta')
}
}
}
class SenhaHasher {
gerarHash(senha) {
return 'hash_' + senha
}
}
class UsuarioRepository {
salvar(usuario) {
console.log('Salvando usuário no banco:', usuario)
}
}
class EmailService {
enviarBoasVindas(email) {
console.log(`Enviando email de boas-vindas para ${email}`)
}
}
class CadastroUsuarioService {
/*
A classe de cadastro não cria dependências dentro dela.
Ela recebe tudo pelo constructor.
*/
constructor(validator, hasher, repository, emailService) {
this.validator = validator
this.hasher = hasher
this.repository = repository
this.emailService = emailService
}
// isso seria ruim, Porque aí a classe fica acoplada.
/*
constructor() {
this.repository = new UsuarioRepositoryMySQL();
}
*/
cadastrar(nome, email, senha) {
this.validator.validar(nome, email, senha)
const senhaCriptografada = this.hasher.gerarHash(senha)
const usuario = {
nome,
email,
senha: senhaCriptografada,
}
this.repository.salvar(usuario)
this.emailService.enviarBoasVindas(email)
return usuario
}
}
const cadastroService = new CadastroUsuarioService(new UsuarioValidator(), new SenhaHasher(), new UsuarioRepository(), new EmailService())
cadastroService.cadastrar('Geraldo', 'contato@geraldo.io', '123456')
/*
No mundo real ficaria algo assim:
UsuarioController.js
CadastroUsuarioService.js
UsuarioValidator.js
UsuarioRepository.js
EmailService.js
SenhaHasher.js
A ideia é:
Quando uma coisa mudar, você altera só uma parte do sistema.
Mudou validação? Mexe no UsuarioValidator.
Mudou banco? Mexe no UsuarioRepository.
Mudou regra de cadastro? Mexe no CadastroUsuarioService.
Mudou envio de email? Mexe no EmailService.
Isso é SOLID na prática.
*/O – Open/Closed Principle – Princípio Aberto/Fechado
a entidade deve estar aberta para extensão, fechada para modificação
exemplo toda vez que aparece uma nova regra de desconto, eu preciso abrir a classe Checkout e mexer nela.
// exemplo
if (discountType === 'birthday') {
return total * 0.85;
}
class Checkout {
calculateTotal(total, discountType) {
if (discountType === 'christmas') {
return total * 0.9; // 10% de desconto
}
if (discountType === 'blackFriday') {
return total * 0.7; // 30% de desconto
}
if (discountType === 'vip') {
return total * 0.8; // 20% de desconto
}
return total;
}
}
const checkout = new Checkout();
console.log(checkout.calculateTotal(100, 'christmas')); // 90
console.log(checkout.calculateTotal(100, 'blackFriday')); // 70
console.log(checkout.calculateTotal(100, 'vip')); // 80Agora o checkout não sabe qual desconto está sendo usado. Ele só sabe que recebeu uma estratégia com o método applyDiscount.
class NoDiscount {
applyDiscount(total) {
return total;
}
}
class ChristmasDiscount {
applyDiscount(total) {
return total * 0.9;
}
}
class BlackFridayDiscount {
applyDiscount(total) {
return total * 0.7;
}
}
class VipDiscount {
applyDiscount(total) {
return total * 0.8;
}
}
class Checkout {
constructor(discountStrategy) {
this.discountStrategy = discountStrategy;
}
calculateTotal(total) {
return this.discountStrategy.applyDiscount(total);
}
}
// uso
const checkoutChristmas = new Checkout(new ChristmasDiscount());
console.log(checkoutChristmas.calculateTotal(100)); // 90
const checkoutBlackFriday = new Checkout(new BlackFridayDiscount());
console.log(checkoutBlackFriday.calculateTotal(100)); // 70
const checkoutVip = new Checkout(new VipDiscount());
console.log(checkoutVip.calculateTotal(100)); // 80
const checkoutNormal = new Checkout(new NoDiscount());
console.log(checkoutNormal.calculateTotal(100)); // 100L – Princípio de Substituição de Liskov (Liskov’s Substitution Principle);
Uma subclasse deve poder ser usada no lugar da classe pai sem alterar o funcionamento esperado.
class Bird {
fly() {
console.log("O pássaro está voando");
}
}
class Sparrow extends Bird {
fly() {
console.log("O pardal está voando");
}
}
class Penguin extends Bird {
fly() {
throw new Error("Pinguins não podem voar");
}
}
function makeBirdFly(bird) {
bird.fly();
}
const sparrow = new Sparrow();
const penguin = new Penguin();
makeBirdFly(sparrow); // OK
makeBirdFly(penguin); // Erro
O problema está aqui:
class Penguin extends Bird
Porque o sistema está dizendo:
Todo Penguin é um Bird, então ele deveria poder ser usado onde um Bird é esperado.class Bird {
eat() {
console.log("O pássaro está comendo");
}
}
class FlyingBird extends Bird {
fly() {
console.log("O pássaro está voando");
}
}
class Sparrow extends FlyingBird {
fly() {
console.log("O pardal está voando");
}
}
class Penguin extends Bird {
swim() {
console.log("O pinguim está nadando");
}
}
function makeBirdFly(bird) {
bird.fly();
}
const sparrow = new Sparrow();
const penguin = new Penguin();
makeBirdFly(sparrow); // OK
// makeBirdFly(penguin); // nem deveria ser permitido conceitualmenteI – interface segregation principle (não parece muito útil)
Agora a função que precisa fazer uma ave voar recebe apenas aves voadoras:
function makeBirdFly(flyingBird) {
flyingBird.fly();
}
const sparrow = new Sparrow();
const eagle = new Eagle();
const penguin = new Penguin();
makeBirdFly(sparrow); // OK
makeBirdFly(eagle); // OK
// makeBirdFly(penguin);
// Não faz sentido, porque Penguin não herda de FlyingBird/*
usando composição fica ainda mais flexível que herança:
*/
class Bird {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} está comendo`);
}
}
const canFly = {
fly() {
console.log(`${this.name} está voando`);
}
};
const canWalk = {
walk() {
console.log(`${this.name} está andando`);
}
};
class Sparrow extends Bird {
constructor() {
super("Pardal");
Object.assign(this, canFly, canWalk);
}
}
class Penguin extends Bird {
constructor() {
super("Pinguim");
Object.assign(this, canWalk);
}
}
const sparrow = new Sparrow();
sparrow.fly(); // Pardal está voando
sparrow.walk(); // Pardal está andando
const penguin = new Penguin();
penguin.walk(); // Pinguim está andando
// penguin.fly();
// Não existe, porque pinguim não tem esse comportamentoD – depencendy intersion principle
class MySQLDatabase:
def save(self, data):
pass
class UserService:
def __init__(self):
self.db = MySQLDatabase()
def register_user(self, user):
self.db.save(user)
Aqui o problema é:
self.db = MySQLDatabase()
O UserService ficou preso ao MySQL.class Database:
def save(self, data):
pass
class MySQLDatabase(Database):
def save(self, data):
pass
class UserService:
def __init__(self, db: Database):
self.db = db
def register_user(self, user):
self.db.save(user)
db = MySQLDatabase()
user_service = UserService(db)