SOLID é um conjunto de cinco princípios de design orientado a objetos que, quando aplicados juntos, resultam em código mais fácil de manter, testar e estender. Vamos ver cada um com exemplos concretos.

S — Single Responsibility

Uma classe deve ter apenas uma razão para mudar.

// ❌ Errado: mistura lógica de negócio e persistência
class UserService {
  createUser(data: CreateUserDTO) {
    const user = new User(data);
    // Valida, salva e envia email — responsabilidades demais
    this.validate(user);
    this.db.save(user);
    this.mailer.sendWelcome(user);
  }
}

// ✅ Correto: cada classe tem um papel
class UserFactory { create(data: CreateUserDTO): User { ... } }
class UserRepository { save(user: User): void { ... } }
class WelcomeMailer { send(user: User): void { ... } }

O — Open/Closed

Aberto para extensão, fechado para modificação.

// ✅ Correto: novos descontos sem modificar a classe existente
interface DiscountStrategy {
  apply(price: number): number;
}

class PercentDiscount implements DiscountStrategy {
  constructor(private pct: number) {}
  apply(price: number) { return price * (1 - this.pct / 100); }
}

class FlatDiscount implements DiscountStrategy {
  constructor(private amount: number) {}
  apply(price: number) { return price - this.amount; }
}

L — Liskov Substitution

Subclasses devem ser substituíveis por suas classes base.

O exemplo clássico é o quadrado/retângulo: um Square que herda de Rectangle e sobrescreve setWidth quebrando o invariante de área — isso viola o LSP.

I — Interface Segregation

Clientes não devem depender de interfaces que não utilizam.

// ❌ Interface gorda
interface Animal {
  walk(): void;
  fly(): void;
  swim(): void;
}

// ✅ Interfaces específicas
interface Walker { walk(): void; }
interface Flyer { fly(): void; }
interface Swimmer { swim(): void; }

class Duck implements Walker, Flyer, Swimmer { ... }
class Dog implements Walker, Swimmer { ... }

D — Dependency Inversion

Dependa de abstrações, não de implementações concretas.

// ✅ O serviço depende da abstração, não do Prisma diretamente
interface UserRepository {
  findById(id: string): Promise<User | null>;
}

class UserService {
  constructor(private repo: UserRepository) {}
  
  async getUser(id: string) {
    return this.repo.findById(id);
  }
}

// Pode injetar PrismaUserRepository, InMemoryUserRepository, etc.

Aplicar SOLID de forma dogmática pode resultar em over-engineering. Use esses princípios como guia, não como regra absoluta. A pergunta certa é sempre: essa abstração resolve um problema real que tenho hoje ou pode surgir amanhã?