Princípios SOLID - Fundamentos do Design Orientado a Objetos

Intermediário | 16 min leitura | 2024.12.24

O que são os Princípios SOLID

SOLID são 5 princípios de design orientado a objetos propostos por Robert C. Martin (Uncle Bob). São diretrizes para projetar software com alta manutenibilidade, extensibilidade e testabilidade.

S - Single Responsibility Principle (Princípio da Responsabilidade Única)
O - Open/Closed Principle (Princípio Aberto/Fechado)
L - Liskov Substitution Principle (Princípio da Substituição de Liskov)
I - Interface Segregation Principle (Princípio da Segregação de Interfaces)
D - Dependency Inversion Principle (Princípio da Inversão de Dependência)

1. Princípio da Responsabilidade Única (SRP)

Uma classe deve ter apenas uma responsabilidade. Deve haver apenas uma razão para mudá-la.

Exemplo de Violação

// Exemplo ruim: Classe com múltiplas responsabilidades
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  // Responsabilidade 1: Gerenciamento de dados do usuário
  getName() { return this.name; }
  setName(name) { this.name = name; }

  // Responsabilidade 2: Operações de banco de dados
  save() {
    db.query('INSERT INTO users ...');
  }

  // Responsabilidade 3: Envio de e-mail
  sendWelcomeEmail() {
    emailService.send(this.email, 'Welcome!');
  }

  // Responsabilidade 4: Serialização JSON
  toJSON() {
    return JSON.stringify({ name: this.name, email: this.email });
  }
}

Exemplo Melhorado

// Bom exemplo: Responsabilidades separadas
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
  getName() { return this.name; }
  setName(name) { this.name = name; }
}

class UserRepository {
  save(user) {
    db.query('INSERT INTO users ...');
  }
}

class UserNotificationService {
  sendWelcomeEmail(user) {
    emailService.send(user.email, 'Welcome!');
  }
}

class UserSerializer {
  toJSON(user) {
    return JSON.stringify({ name: user.name, email: user.email });
  }
}

2. Princípio Aberto/Fechado (OCP)

Software deve estar aberto para extensão, mas fechado para modificação.

Exemplo de Violação

// Exemplo ruim: Necessidade de modificação a cada novo método de pagamento
class PaymentProcessor {
  processPayment(type, amount) {
    if (type === 'credit') {
      // Processamento de cartão de crédito
    } else if (type === 'debit') {
      // Processamento de cartão de débito
    } else if (type === 'paypal') {
      // Processamento PayPal
    }
    // Adicionar novo método de pagamento requer modificar este método
  }
}

Exemplo Melhorado

// Bom exemplo: Aberto para extensão
class PaymentProcessor {
  constructor(paymentMethod) {
    this.paymentMethod = paymentMethod;
  }

  processPayment(amount) {
    return this.paymentMethod.process(amount);
  }
}

// Cada método de pagamento é uma classe independente
class CreditCardPayment {
  process(amount) { /* Processamento de cartão de crédito */ }
}

class PayPalPayment {
  process(amount) { /* Processamento PayPal */ }
}

// Adicionar novo método de pagamento é possível sem alterar código existente
class CryptoPayment {
  process(amount) { /* Processamento de criptomoeda */ }
}

// Uso
const processor = new PaymentProcessor(new CryptoPayment());
processor.processPayment(1000);

3. Princípio da Substituição de Liskov (LSP)

Classes derivadas devem ser substituíveis por suas classes base.

Exemplo de Violação

// Exemplo ruim: Quadrado é um tipo de retângulo, mas comportamento é diferente
class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  setWidth(width) { this.width = width; }
  setHeight(height) { this.height = height; }
  getArea() { return this.width * this.height; }
}

class Square extends Rectangle {
  setWidth(width) {
    this.width = width;
    this.height = width; // Quadrado, então altura também muda
  }

  setHeight(height) {
    this.width = height; // Quadrado, então largura também muda
    this.height = height;
  }
}

// Código problemático
function increaseRectangleWidth(rect) {
  rect.setWidth(rect.width + 1);
  // Se Rectangle: area = (width + 1) * height
  // Se Square: area = (width + 1) * (width + 1) ← Resultado inesperado
}

Exemplo Melhorado

// Bom exemplo: Usar interface comum
class Shape {
  getArea() { throw new Error('Not implemented'); }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }
  getArea() { return this.width * this.height; }
}

class Square extends Shape {
  constructor(side) {
    super();
    this.side = side;
  }
  getArea() { return this.side * this.side; }
}

4. Princípio da Segregação de Interfaces (ISP)

Clientes não devem ser forçados a depender de métodos que não usam.

Exemplo de Violação

// Exemplo ruim: Interface muito grande
class Worker {
  work() { /* trabalhar */ }
  eat() { /* comer */ }
  sleep() { /* dormir */ }
}

class Robot extends Worker {
  work() { /* trabalhar */ }
  eat() { throw new Error('Robots do not eat'); } // Desnecessário
  sleep() { throw new Error('Robots do not sleep'); } // Desnecessário
}

Exemplo Melhorado

// Bom exemplo: Interfaces separadas
class Workable {
  work() { throw new Error('Not implemented'); }
}

class Eatable {
  eat() { throw new Error('Not implemented'); }
}

class Sleepable {
  sleep() { throw new Error('Not implemented'); }
}

class Human {
  work() { /* trabalhar */ }
  eat() { /* comer */ }
  sleep() { /* dormir */ }
}

class Robot {
  work() { /* trabalhar */ }
  // eat() e sleep() não são necessários
}

5. Princípio da Inversão de Dependência (DIP)

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

Exemplo de Violação

// Exemplo ruim: Dependência direta de implementação concreta
class UserService {
  constructor() {
    this.database = new MySQLDatabase(); // Dependência de implementação concreta
  }

  getUser(id) {
    return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
  }
}

Exemplo Melhorado

// Bom exemplo: Dependência de abstração (interface)
class Database {
  query(sql) { throw new Error('Not implemented'); }
}

class MySQLDatabase extends Database {
  query(sql) { /* Implementação MySQL */ }
}

class PostgreSQLDatabase extends Database {
  query(sql) { /* Implementação PostgreSQL */ }
}

class UserService {
  constructor(database) {
    this.database = database; // Dependência de abstração (injeção de dependência)
  }

  getUser(id) {
    return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
  }
}

// Uso
const userService = new UserService(new PostgreSQLDatabase());

// Durante testes
const mockDatabase = { query: jest.fn() };
const testService = new UserService(mockDatabase);

Praticando SOLID

Checklist

□ A classe tem apenas uma responsabilidade? (SRP)
□ Novas funcionalidades podem ser adicionadas sem modificar código existente? (OCP)
□ Classes derivadas podem ser usadas no lugar de classes base? (LSP)
□ As interfaces são mínimas? (ISP)
□ Depende de abstrações ao invés de implementações concretas? (DIP)

Equilíbrio na Aplicação

Evite aplicação excessiva:
- Não aplique padrões complexos para problemas simples
- Tenha em mente YAGNI (You Ain't Gonna Need It)
- Refatore gradualmente conforme necessário

Resumo

Os princípios SOLID são diretrizes para projetar software com alta manutenibilidade e extensibilidade. Não é necessário aplicar todos os princípios estritamente o tempo todo, mas mantê-los em mente como critérios de design permite escrever código melhor.

← Voltar para a lista