SOLIDClean CodeArquitecturaTypeScript

SOLID: Los 5 Principios que Todo Arquitecto Debe Dominar

Lewis Lopez
SOLID: Los 5 Principios que Todo Arquitecto Debe Dominar

Tras 10+ años construyendo sistemas para la banca, he visto código de todo tipo. El código que sobrevive no es el más ingenioso, es el más mantenible.

Los principios SOLID no son teoría académica. Son reglas de supervivencia en entornos de producción reales.

Por Qué SOLID Importa (Y Mucho)

Imagina este escenario: tu equipo hereda un monolito de 500K líneas. Cada cambio rompe algo. Los tests son inexistentes. Nadie entiende el código completo.

Este es el costo de ignorar SOLID.

En el sector bancario, un bug puede costar millones. No hay espacio para “moverle y ver qué pasa”. Necesitas arquitectura que puedas:

  • Entender en segundos
  • Modificar sin miedo
  • Testear en aislamiento
  • Escalar sin refactoring masivo

Los 5 Principios (Aplicados al Mundo Real)

1. Single Responsibility Principle (SRP)

“Una clase debe tener una, y solo una, razón para cambiar”

Ejemplo Real: Servicio de Usuarios

// ❌ MAL: Demasiadas responsabilidades
class UserService {
  createUser(data: CreateUserDto) { }
  sendWelcomeEmail(email: string) { }
  logUserActivity(userId: string) { }
  generateReport() { }
}

// ✅ BIEN: Separación de responsabilidades
class UserService {
  createUser(data: CreateUserDto) { }
}

class EmailService {
  sendWelcomeEmail(email: string) { }
}

class ActivityLogger {
  logUserActivity(userId: string) { }
}

class ReportGenerator {
  generateUserReport() { }
}

Por qué importa: Si cambias la lógica de emails, no tocas la lógica de usuarios. Menor riesgo de bugs colaterales.

2. Open/Closed Principle (OCP)

“Abierto para extensión, cerrado para modificación”

Ejemplo Real: Sistema de Pagos

// ❌ MAL: Modificar código existente
class PaymentProcessor {
  process(type: string, amount: number) {
    if (type === 'credit_card') {
      // lógica tarjeta
    } else if (type === 'paypal') {
      // lógica PayPal
    } else if (type === 'crypto') {
      // ¿Agregamos otro if?
    }
  }
}

// ✅ BIEN: Extensión sin modificación
interface PaymentMethod {
  process(amount: number): Promise<void>;
}

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

class PayPalPayment implements PaymentMethod {
  async process(amount: number) { }
}

class CryptoPayment implements PaymentMethod {
  async process(amount: number) { }
}

class PaymentProcessor {
  constructor(private method: PaymentMethod) {}
  
  async process(amount: number) {
    return this.method.process(amount);
  }
}

Por qué importa: Agregar nuevos métodos de pago no requiere modificar código existente. Cero riesgo de romper lo que ya funciona.

3. Liskov Substitution Principle (LSP)

“Las subclases deben ser sustituibles por sus clases base”

Ejemplo Real: Repositorios de Datos

// ❌ MAL: Rompe el contrato
abstract class Repository {
  abstract findById(id: string): Promise<Entity>;
}

class InMemoryRepository extends Repository {
  async findById(id: string) {
    throw new Error('Not implemented'); // 💥 Rompe contrato
  }
}

// ✅ BIEN: Respeta el contrato
abstract class Repository {
  abstract findById(id: string): Promise<Entity | null>;
}

class InMemoryRepository extends Repository {
  async findById(id: string): Promise<Entity | null> {
    return this.storage.get(id) ?? null; // ✅ Cumple contrato
  }
}

class MongoRepository extends Repository {
  async findById(id: string): Promise<Entity | null> {
    return this.model.findById(id); // ✅ Cumple contrato
  }
}

Por qué importa: Puedes intercambiar implementaciones (In-Memory en tests, Mongo en producción) sin romper nada.

4. Interface Segregation Principle (ISP)

“Los clientes no deben depender de interfaces que no usan”

Ejemplo Real: Servicios de Notificación

// ❌ MAL: Interfaz monolítica
interface NotificationService {
  sendEmail(to: string, body: string): void;
  sendSMS(phone: string, message: string): void;
  sendPush(deviceId: string, title: string): void;
  sendWebhook(url: string, payload: object): void;
}

// ✅ BIEN: Interfaces segregadas
interface EmailSender {
  sendEmail(to: string, body: string): void;
}

interface SMSSender {
  sendSMS(phone: string, message: string): void;
}

interface PushSender {
  sendPush(deviceId: string, title: string): void;
}

// Cada clase implementa solo lo que necesita
class UserNotifier implements EmailSender, SMSSender {
  sendEmail(to: string, body: string) { }
  sendSMS(phone: string, message: string) { }
}

Por qué importa: No obligas a implementar métodos innecesarios. Menos código muerto, interfaces más claras.

5. Dependency Inversion Principle (DIP)

“Depende de abstracciones, no de implementaciones concretas”

Ejemplo Real: Servicio de Autenticación

// ❌ MAL: Dependencia directa
class AuthService {
  private db = new PostgresDatabase(); // 💥 Acoplamiento fuerte
  
  async login(email: string, password: string) {
    return this.db.query('SELECT...');
  }
}

// ✅ BIEN: Inyección de dependencias
interface UserRepository {
  findByEmail(email: string): Promise<User | null>;
}

class AuthService {
  constructor(
    private userRepo: UserRepository, // ✅ Abstracción
    private hasher: PasswordHasher,
  ) {}
  
  async login(email: string, password: string) {
    const user = await this.userRepo.findByEmail(email);
    // ...
  }
}

Por qué importa: Puedes testear con mocks, cambiar de base de datos, o usar repositorios in-memory sin modificar AuthService.

SOLID en NestJS (Mi Stack Favorito)

NestJS está diseñado con SOLID en mente:

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepo: Repository<User>, // DIP
    private emailService: EmailService, // DIP
  ) {}
  
  async createUser(dto: CreateUserDto) {
    const user = this.userRepo.create(dto); // SRP
    await this.userRepo.save(user);
    await this.emailService.sendWelcome(user.email); // SRP
    return user;
  }
}

Todo está desacoplado, testeable y escalable.

El Resultado Real

En un proyecto bancario que lideré:

  • Antes de SOLID: 3 días para agregar un nuevo tipo de transacción
  • Después de SOLID: 2 horas

La diferencia: código que se extiende en lugar de modificarse.

¿Cómo Empezar?

  1. Identifica clases con múltiples responsabilidades → Aplica SRP
  2. Busca if/else gigantes → Aplica OCP con interfaces
  3. Revisa tus constructores → Aplica DIP con inyección de dependencias

No intentes aplicar todo de golpe. Empieza con SRP y OCP. Son el 80% del valor.

Próximo Paso

Si quieres dominar SOLID con proyectos reales (no ejercicios de juguete), mi curso TypeScript: Arquitectura y Clean Code te lleva desde cero hasta aplicar estos principios en producción.

Incluye:

  • ✅ Casos reales del sector bancario
  • ✅ Refactoring de código legacy
  • ✅ Tests automáticos con TDD
  • ✅ Revisión de código 1-on-1

Descarga “El Arquitecto Checklist” gratis y descubre tu nivel actual.


¿Preguntas sobre SOLID? Respondo en los comentarios 👇

Lewis Lopez
Arquitecto Senior | Fundador MATIAS IMPULSO
10+ años en el sector bancario

Compartir este artículo:

¿Te gustó este artículo?

Aprende arquitectura de élite y transforma tu carrera de programador 1x a arquitecto 10x.