Saltar al contenidoJMRG
Todas las entradas

Principios SOLID: La Base de la Arquitectura Limpia

solidclean-architecturedesign-principles

¿Qué es SOLID?

SOLID es un acrónimo que representa cinco principios de diseño de software propuestos por Robert C. Martin (Uncle Bob). Estos principios guían la creación de código limpio y arquitecturas sostenibles, permitiendo construir sistemas que son fáciles de mantener, extender y testear.

graph TB
    SOLID[SOLID Principles]
    S[S - Single Responsibility]
    O[O - Open/Closed]
    L[L - Liskov Substitution]
    I[I - Interface Segregation]
    D[D - Dependency Inversion]

    SOLID --> S
    SOLID --> O
    SOLID --> L
    SOLID --> I
    SOLID --> D

    S --> S1[Una razón para cambiar]
    O --> O1[Abierto a extensión]
    O --> O2[Cerrado a modificación]
    L --> L1[Subtipos sustituibles]
    I --> I1[Interfaces específicas]
    D --> D1[Depender de abstracciones]

Los 5 Principios

S - Single Responsibility Principle (SRP)

Una clase debe tener una única razón para cambiar.

Este principio establece que cada módulo o clase debe ser responsable de una sola parte de la funcionalidad del software.

Antes: Una clase con múltiples responsabilidades

class UserService {
  createUser(userData: UserData): User { }
  sendWelcomeEmail(user: User): void { }
  generateReport(users: User[]): Report { }
  validateUserData(data: UserData): boolean { }
}

Después: Cada clase tiene una responsabilidad

/** Repositorio para operaciones de persistencia de usuarios */
class UserRepository {
  create(userData: UserData): User { }
  findById(id: string): User | null { }
}
/** Servicio de notificaciones por email */
class EmailService {
  sendWelcomeEmail(user: User): void { }
}
/** Validador de datos de usuario */
class UserValidator {
  validate(data: UserData): ValidationResult { }
}

O - Open/Closed Principle (OCP)

Las entidades de software deben estar abiertas para extensión, pero cerradas para modificación.

Debemos poder agregar nueva funcionalidad sin cambiar el código existente.

Antes: Necesita modificarse para cada nuevo tipo

class PaymentProcessor {
  process(payment: Payment): void {
    if (payment.type === 'credit') {
      this.processCreditCard(payment);
    } else if (payment.type === 'paypal') {
      this.processPayPal(payment);
    }
  }
}

Después: Extensible sin modificación

/** Estrategia de pago - interfaz base */
interface PaymentStrategy {
  process(amount: number): Promise<PaymentResult>;
}
class CreditCardPayment implements PaymentStrategy {
  async process(amount: number): Promise<PaymentResult> { }
}
class PayPalPayment implements PaymentStrategy {
  async process(amount: number): Promise<PaymentResult> { }
}
/** Procesador que acepta cualquier estrategia */
class PaymentProcessor {
  constructor(private strategy: PaymentStrategy) {}
  async process(amount: number): Promise<PaymentResult> {
    return this.strategy.process(amount);
  }
}

L - Liskov Substitution Principle (LSP)

Los subtipos deben ser sustituibles por sus tipos base.

Si S es un subtipo de T, entonces los objetos de tipo T pueden ser reemplazados por objetos de tipo S sin alterar las propiedades del programa.

Antes: Square rompe el contrato de Rectangle

class Rectangle {
  constructor(protected width: number, protected height: number) {}
  setWidth(width: number): void { this.width = width; }
  setHeight(height: number): void { this.height = height; }
  getArea(): number { return this.width * this.height; }
}
class Square extends Rectangle {
  setWidth(width: number): void {
    this.width = width;
    this.height = width;
  }
}

Después: Interfaces apropiadas para cada forma

/** Contrato base para figuras geométricas */
interface Shape {
  getArea(): number;
}
class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}
  getArea(): number { return this.width * this.height; }
}
class Square implements Shape {
  constructor(private side: number) {}
  getArea(): number { return this.side * this.side; }
}

I - Interface Segregation Principle (ISP)

Interfaces específicas son mejores que una interfaz general.

Los clientes no deberían verse forzados a depender de interfaces que no usan.

Antes: Interfaz demasiado grande

interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
  attendMeeting(): void;
}

Después: Interfaces segregadas

interface Workable { work(): void; }
interface Eatable { eat(): void; }
interface Sleepable { sleep(): void; }
class Human implements Workable, Eatable, Sleepable {
  work(): void { }
  eat(): void { }
  sleep(): void { }
}
class Robot implements Workable {
  work(): void { }
}

D - Dependency Inversion Principle (DIP)

Depender de abstracciones, no de concreciones.

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

Antes: Alto nivel depende de bajo nivel

class UserService {
  private database = new MySQLDatabase();
  getUser(id: string): User {
    return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
  }
}

Después: Depende de abstracciones

/** Abstracción de base de datos */
interface Database {
  query<T>(sql: string): T;
}
class UserService {
  constructor(private database: Database) {}
  getUser(id: string): User {
    return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
  }
}
const mysqlService = new UserService(new MySQLDatabase());
const postgresService = new UserService(new PostgresDatabase());
const mockService = new UserService(new MockDatabase());

Beneficios de Aplicar SOLID

  1. Mantenibilidad - Código más fácil de entender y modificar
  2. Testabilidad - Componentes aislados facilitan las pruebas
  3. Extensibilidad - Añadir funcionalidad sin romper lo existente
  4. Reutilización - Componentes más modulares y reutilizables
  5. Legibilidad - Código autoexplicativo y bien organizado

Conclusión

Los principios SOLID no son reglas rígidas, sino guías para tomar mejores decisiones de diseño. Aplicarlos requiere práctica y juicio - a veces un diseño más simple es preferible a uno que sigue SOLID al pie de la letra.

En el próximo artículo exploraremos el Patrón Observer, uno de los patrones de comportamiento más utilizados en sistemas reactivos y event-driven.


Basado en los principios de Robert C. Martin (Uncle Bob) y "Clean Architecture".