SOLID

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
 ├──  suporte
 ├── vende
 └── ainda limpa o escritório

Funciona 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'));         // 80

Agora 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)); // 100

L – 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 conceitualmente

I – 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 comportamento

D – 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)

ref: Princípios SOLID Pelos Olhos de um Dev Sr.

Scroll to Top