SOLID es una sigla que todo desarrollador conoce pero pocos aplican de forma consistente. Los cinco principios existen porque el código que los viola se vuelve difícil de cambiar, testear y extender. Aquí está cada uno con un problema real y la solución.

S: Single Responsibility Principle

Una clase o función debe tener un motivo para cambiar. Cuando una función hace demasiadas cosas, cualquier cambio en una de ellas arriesga romper las otras.

// ❌ Hace demasiado: cambia por múltiples motivos
class UserService {
  async createUser(data: CreateUserDto) {
    const user = await this.db.user.create({ data })
    await this.sendWelcomeEmail(user.email)
    await this.auditLog('user.created', user.id)
    return user
  }
}

// ✅ Cada responsabilidad separada
class UserService {
  constructor(
    private readonly userRepo: UserRepository,
    private readonly emailService: EmailService,
    private readonly auditService: AuditService
  ) {}

  async createUser(data: CreateUserDto) {
    const user = await this.userRepo.create(data)
    await this.emailService.sendWelcome(user.email)
    await this.auditService.log('user.created', user.id)
    return user
  }
}

La versión correcta tiene tres razones para cambiar: repositorio de datos, template de email o política de auditoría. La versión errónea tiene una razón por cada cambio de cualquiera de esas tres.

O: Open/Closed Principle

Las entidades de software deben estar abiertas para extensión y cerradas para modificación. Añade comportamiento sin alterar código existente.

// ❌ Para agregar tipo de pago, modifica la clase
class PaymentProcessor {
  process(type: string, amount: number) {
    if (type === 'credit_card') { /* ... */ }
    else if (type === 'pix') { /* ... */ }
    // agregar 'boleto' requiere modificar este método
  }
}

// ✅ Añade comportamiento sin modificar código existente
interface PaymentMethod {
  process(amount: number): Promise<PaymentResult>
}

class CreditCardPayment implements PaymentMethod {
  async process(amount: number) { /* ... */ }
}

class PixPayment implements PaymentMethod {
  async process(amount: number) { /* ... */ }
}

// Para agregar BoletoPayment, crea una nueva clase
// Ningún código existente se modifica

L: Liskov Substitution Principle

Los subtipos deben ser sustituibles por sus tipos base sin alterar el comportamiento correcto del programa. Si necesitas instanceof para decidir comportamiento, violaste LSP.

// ❌ Subtipo rompiendo contrato
class Rectangle {
  constructor(protected width: number, protected height: number) {}
  setWidth(w: number) { this.width = w }
  setHeight(h: number) { this.height = h }
  area() { return this.width * this.height }
}

class Square extends Rectangle {
  setWidth(w: number) { this.width = w; this.height = w }
  setHeight(h: number) { this.width = h; this.height = h }
}

// Square no puede ser sustituido por Rectangle sin romper expectativas
function increaseWidth(rect: Rectangle) {
  rect.setWidth(rect.width + 1) // Square cambia height también
}

// ✅ Composición o jerarquía que preserva contrato
interface Shape {
  area(): number
  scale(factor: number): Shape
}

I: Interface Segregation Principle

Los clientes no deben ser forzados a depender de interfaces que no usan. Interfaces grandes fuerzan implementaciones que contienen código muerto.

// ❌ Interface gigante: fuerza implementaciones incompletas
interface DataStore {
  read(id: string): Promise<any>
  write(id: string, data: any): Promise<void>
  delete(id: string): Promise<void>
  subscribe(event: string, cb: Function): void
  connect(): Promise<void>
}

// ✅ Interfaces segregadas
interface Reader {
  read(id: string): Promise<any>
}

interface Writer {
  write(id: string, data: any): Promise<void>
}

interface EventSource {
  subscribe(event: string, cb: Function): void
}

// Cada implementación depende solo de lo que usa

D: Dependency Inversion Principle

Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones. Las abstracciones no deben depender de detalles. Los detalles deben depender de abstracciones.

// ❌ Alto nivel depende de bajo nivel
class OrderService {
  constructor() {
    this.db = new PrismaClient()  // acoplamiento directo
  }
}

// ✅ Ambos dependen de abstracción
interface OrderRepository {
  save(order: Order): Promise<void>
  findById(id: string): Promise<Order | null>
}

class OrderService {
  constructor(private readonly repo: OrderRepository) {}
}

// PrismaOrderRepository implementa OrderRepository
// En tests, usa InMemoryOrderRepository

Cómo aplicarlo en el día a día

SOLID no es para aplicar en todo el código. Es para aplicar donde el costo de cambio es alto: dominio de negocio, capas de servicio, puntos de extensión. Utility functions, helpers simples y código de configuración no necesitan SOLID.

La pregunta que guía: "Si necesito cambiar X, cuántas otras cosas van a romperse?" Si la respuesta es muchas, refactoriza. Si son pocas, está lo bastante bien.