🎯 Aprende los fundamentos del diseño orientado a objetos

S.O.L.I.D.

Los 5 principios que transformarán tu código en un castillo de naipes bien construido — flexible, mantenible y elegante. Una guía interactiva para developers que quieren escribir código que funcione hoy y sea fácil de cambiar mañana.

🚀 Comenzar viaje
S - Responsabilidad Única O - Abierto/Cerrado L - Sustitución de Liskov I - Segregación de Interfaces D - Inversión de Dependencias

Mapa de aprendizaje

Cada principio es un pilar. Todos juntos forman el fantástico edificio del código limpio.

S

SRP

Responsabilidad Única

O

OCP

Abierto / Cerrado

L

LSP

Sustitución de Liskov

I

ISP

Segregación de Interfaces

D

DIP

Inversión de Dependencias

S Principio 1

Responsabilidad Única

"Una clase debería tener una sola razón para cambiar." — Robert C. Martin

🎯 ¿Qué es SRP?

El Principio de Responsabilidad Única (Single Responsibility Principle) dice que cada clase o módulo debe tener una y solo una razón para cambiar. En otras palabras: cada pieza de código debe hacer una cosa, y hacerla bien.

🍕 Analogía del restaurante: Imagina un restaurante donde el camarero cocina, sirve, cobra y friega los platos. Cuando el lavaplatos se estropea, ¡todo el restaurante para! En un buen restaurante, cada rol tiene una responsabilidad: el chef cocina, el camarero sirve, el cajero cobra. Si algo falla, solo afecta a esa área.

Cuando una clase acumula demasiadas responsabilidades, los cambios en una funcionalidad pueden romper otras no relacionadas. El código se vuelve frágil, difícil de probar y de mantener.

❌ MAL Clase Usuario datos + email + BD + informes 🧑‍🍳 Una clase lo hace todo ✅ BIEN 📦 Usuario (datos) 📧 EmailService (envíos) 🗄️ UserRepository (BD) 📊 ReportGenerator (informes) 🎯 Cada clase, una responsabilidad
Comparativa: una clase con múltiples responsabilidades vs. clases especializadas
// ❌ VIOLACIÓN SRP: La clase Usuario hace de todo
class Usuario {
  constructor(nombre, email) {
    this.nombre = nombre;
    this.email = email;
  }

  // Responsabilidad 1: datos
  getDatos() {
    return { nombre: this.nombre, email: this.email };
  }

  // Responsabilidad 2: persistencia
  guardarEnBD() {
    console.log(`Guardando ${this.nombre} en MySQL...`);
  }

  // Responsabilidad 3: notificaciones
  enviarEmail(asunto, cuerpo) {
    console.log(`Enviando email a ${this.email}: ${asunto}`);
  }

  // Responsabilidad 4: reportes
  generarReporte() {
    return `Reporte de ${this.nombre}`;
  }
}

// Si cambia la BD, toca modificar Usuario
// Si cambia el email, también. ¡Caos!
// ✅ SRP APLICADO: Cada clase tiene una sola responsabilidad

// Solo datos del usuario
class Usuario {
  constructor(nombre, email) {
    this.nombre = nombre;
    this.email = email;
  }
}

// Solo persistencia
class UsuarioRepository {
  guardar(usuario) {
    console.log(`Guardando ${usuario.nombre} en MySQL...`);
  }
}

// Solo notificaciones
class EmailService {
  enviar(usuario, asunto, cuerpo) {
    console.log(`Email a ${usuario.email}: ${asunto}`);
  }
}

// Solo reportes
class ReporteGenerator {
  generar(usuario) {
    return `Reporte de ${usuario.nombre}`;
  }
}

// Cada clase cambia por una sola razón. 💪

🧪 SRP Pruébalo tú mismo

// Haz clic en "Ejecutar" para ver el resultado
🤔 ¿Cuál de los siguientes es un síntoma claro de que se está violando SRP?
A La clase tiene más de 5 métodos
B La clase importa módulos de dominios muy distintos (BD, email, UI)
C La clase usa herencia
D La clase tiene un método privado
O Principio 2

Abierto / Cerrado

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

🔌 ¿Qué es OCP?

El Principio Abierto/Cerrado (Open/Closed Principle) establece que debes poder extender el comportamiento de una clase sin modificar su código fuente. ¿Cómo se logra? Principalmente mediante abstracciones: interfaces, clases base o estrategias.

🔌 Analogía del enchufe: Tu casa tiene enchufes (abiertos para extensión). Puedes conectar un ventilador, un cargador o una aspiradora sin modificar el enchufe. El enchufe está "cerrado" (no lo modificas) pero acepta nuevos dispositivos. Eso es OCP: diseña un sistema que acepte nuevos comportamientos sin tocar lo que ya funciona.

Sin OCP, cada vez que llega un nuevo requerimiento tienes que abrir una clase existente y añadir un if más. Con OCP, creas una nueva clase que implementa una interfaz y el sistema la acepta sin cambios.

«abstract» MetodoPago + pagar(monto) TarjetaCredito PayPal Bitcoin ProcesadorPago.procesar(metodo, monto) ✔ Nueva moneda → nueva clase
Herencia polimórfica: el procesador acepta cualquier nuevo método de pago sin cambios
// ❌ VIOLACIÓN OCP: Cada nuevo método = modificar la clase
class ProcesadorPagos {
  procesar(tipo, monto) {
    if (tipo === 'tarjeta') {
      console.log(`Procesando ${monto}€ con tarjeta...`);
    } else if (tipo === 'paypal') {
      console.log(`Procesando ${monto}€ con PayPal...`);
    } else if (tipo === 'bitcoin') {
      console.log(`Procesando ${monto}€ con Bitcoin...`);
    } else {
      throw new Error('Método no soportado');
    }
  }
}

// Para añadir "cripto": abrimos ProcesadorPagos y añadimos otro else if
// ⚠️ Riesgo de romper métodos existentes
// ✅ OCP APLICADO: Nuevos métodos = nuevas clases, no modificaciones

// Abstracción cerrada
class MetodoPago {
  pagar(monto) {
    throw new Error('Método no implementado');
  }
}

// Extensiones :)
class TarjetaCredito extends MetodoPago {
  pagar(monto) {
    console.log(`💳 Pagando ${monto}€ con tarjeta...`);
  }
}

class PayPal extends MetodoPago {
  pagar(monto) {
    console.log(`🅿️ Pagando ${monto}€ con PayPal...`);
  }
}

class ProcesadorPagos {
  procesar(metodo, monto) {
    metodo.pagar(monto); // No necesita saber el tipo concreto
  }
}

// Para añadir "CryptoWallet":
// class CryptoWallet extends MetodoPago { pagar(monto) { ... } }
// ¡Sin tocar ProcesadorPagos! 🎉

🧪 OCP Extiende sin modificar

// Haz clic en "Ejecutar" para ver el resultado
🤔 ¿Qué ventaja principal aporta OCP en un proyecto real?
A El código se ejecuta más rápido
B Las clases tienen menos métodos
C Añadir funcionalidad nueva no requiere modificar código existente
D Elimina la necesidad de usar herencia
L Principio 3

Sustitución de Liskov

"Si S es un subtipo de T, entonces los objetos de T pueden reemplazarse con objetos de S sin alterar las propiedades del programa." — Barbara Liskov

🔄 ¿Qué es LSP?

El Principio de Sustitución de Liskov (Liskov Substitution Principle) dice que una subclase debe poder reemplazar a su clase base sin que el programa se comporte de forma inesperada. Si tienes una función que funciona con una clase padre, debería funcionar igual con cualquier subclase.

🦆 Analogía del pato de goma: Si tu programa espera un "Pato" que hace "Cuac" y nada, un "PatoDeGoma" que hace "Cuac" pero se hunde NO es un sustituto válido. LSP va de contratos: las subclases deben respetar el comportamiento prometido por la clase base.

El ejemplo clásico de violación es Cuadrado extends Rectángulo. Un rectángulo permite cambiar alto y ancho independientemente; un cuadrado no. Si tu código espera un rectángulo y recibe un cuadrado, el comportamiento se rompe.

❌ Violación LSP Rectangulo setAncho(w) / setAlto(h) Cuadrado ⚠️ setAlto() también cambia el ancho Si el cliente espera Rectangulo
y recibe Cuadrado → ¡explota! ✅ LSP correcto Forma + area() Rectangulo Cuadrado area(): ancho × alto area(): lado × lado ✔ Cada uno implementa area() a su manera
Izquierda: Cuadrado rompe el contrato de Rectángulo. Derecha: diseño correcto sin herencia tramposa
// ❌ VIOLACIÓN LSP: Cuadrado no es un Rectángulo válido

class Rectangulo {
  constructor(ancho, alto) {
    this.ancho = ancho;
    this.alto = alto;
  }
  setAncho(w) { this.ancho = w; }
  setAlto(h) { this.alto = h; }
  area() { return this.ancho * this.alto; }
}

class Cuadrado extends Rectangulo {
  setAncho(w) { this.ancho = w; this.alto = w; }
  setAlto(h) { this.alto = h; this.ancho = h; }
}

// Cliente que espera un Rectángulo:
function redimensionar(rect) {
  rect.setAncho(5);
  rect.setAlto(10);
  console.log(`Área esperada: 50, obtenida: ${rect.area()}`);
}

redimensionar(new Rectangulo(2, 3)); // 50 ✓
redimensionar(new Cuadrado(2, 3));    // 100 ✗ ¡Estalla!
// ✅ LSP APLICADO: abstracción común sin trampas

// Clase base abstracta (contrato claro)
class Forma {
  area() { throw new Error('Implementa area()'); }
}

class Rectangulo extends Forma {
  constructor(w, h) { super(); this.ancho = w; this.alto = h; }
  area() { return this.ancho * this.alto; }
}

class Cuadrado extends Forma {
  constructor(lado) { super(); this.lado = lado; }
  area() { return this.lado * this.lado; }
}

// Cliente inocente:
function imprimirArea(forma) {
  console.log(`Área: ${forma.area()}`);
}

imprimirArea(new Rectangulo(4, 5)); // 20 ✓
imprimirArea(new Cuadrado(4));     // 16 ✓
// Ambos se comportan como Formas. Sin sorpresas. ✨

🧪 LSP Sustituye sin romper

// Haz clic en "Ejecutar" para ver el resultado
🤔 Si una subclase lanza una excepción que la clase base no lanza, ¿está violando LSP?
A No, es normal que las subclases tengan comportamientos distintos
B Sí, porque está debilitando el contrato de la clase base
C Solo si la excepción es de tipo Error
D Depende del lenguaje de programación
I Principio 4

Segregación de Interfaces

"Ningún cliente debe ser forzado a depender de interfaces que no utiliza." — Robert C. Martin

✂️ ¿Qué es ISP?

El Principio de Segregación de Interfaces (Interface Segregation Principle) recomienda que las interfaces sean pequeñas y específicas, no grandes y genéricas. Es mejor tener varias interfaces específicas que una interfaz "todo-en-uno" que obligue a las clases a implementar métodos que no necesitan.

🍽️ Analogía del menú del restaurante: Imagina que en un restaurante te obligan a pagar un menú degustación completo cuando solo quieres un café. Sería absurdo. ISP dice: ofrece interfaces (menús) pequeños y enfocados. El cliente de café pide café, el cliente de cena pide cena. Cada cliente depende solo de lo que necesita.

En la práctica, esto evita que clases "Robots" tengan que implementar comer() o que clases "PatosDeGoma" implementen volar(). Interfaces más pequeñas = código más limpio y desacoplado.

❌ Interfaz hinchada Trabajador trabajar() | comer() | dormir() Humano todos los métodos ✔ Robot comer() y dormir() ✗ ✅ Interfaces segregadas Trabajable: trabajar() Comible: comer() Dormible: dormir() Humano Trabajable+Comible+Dormible Robot Solo Trabajable ✔
Dividir interfaces grandes en pequeñas y específicas evita implementaciones forzadas
// ❌ VIOLACIÓN ISP: Una interfaz enorme obliga a implementar de todo

// "Interfaz" representada como clase base
class Trabajador {
  trabajar() { throw new Error('implementar'); }
  comer() { throw new Error('implementar'); }
  dormir() { throw new Error('implementar'); }
}

class Humano extends Trabajador {
  trabajar() { console.log('Trabajando 💼'); }
  comer() { console.log('Comiendo 🍔'); }
  dormir() { console.log('Durmiendo 😴'); } // Todo bien aquí
}

class Robot extends Trabajador {
  trabajar() { console.log('Trabajando 🤖'); }
  comer() { throw new Error('Los robots no comen 🤦'); } // Forzado
  dormir() { console.log('Apagando...'); } // "dormir" conceptualmente extraño
}

// El Robot se ve obligado a implementar métodos que no tienen sentido.
// ✅ ISP APLICADO: Interfaces pequeñas y con propósito

// Cada interfaz conceptual es independiente
class Trabajable {
  trabajar() { throw new Error('implementar'); }
}
class Comible {
  comer() { throw new Error('implementar'); }
}
class Dormible {
  dormir() { throw new Error('implementar'); }
}

class Humano extends Trabajable {
  trabajar() { console.log('Diseñando UI'); }
}
// Humano también necesitaría Comible, Dormible — composición al rescate

class Robot extends Trabajable {
  trabajar() { console.log('Ensamble piezas'); }
}

// Robot solo implementa lo que necesita. Humano puede componer
// las interfaces que requiera. Cada clase es honesta sobre lo que hace. ✨

🧪 ISP Interfaces a medida

// Haz clic en "Ejecutar" para ver el resultado
🤔 ¿Cuál es la señal más clara de que necesitas aplicar ISP?
A Una clase tiene más de 10 métodos
B Una clase lanza "No implementado" o "No soportado" en varios métodos
C Las interfaces tienen nombres muy largos
D Hay muchas clases en el proyecto
D Principio 5

Inversión de Dependencias

"Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones." — Robert C. Martin

🔀 ¿Qué es DIP?

El Principio de Inversión de Dependencias (Dependency Inversion Principle) nos dice dos cosas: (1) los módulos de alto nivel (negocio) no deben depender de módulos de bajo nivel (infraestructura), y (2) ambos deben depender de abstracciones. Además, las abstracciones no deben depender de detalles; los detalles deben depender de las abstracciones.

🔌 Analogía del cargador USB: Tu móvil (alto nivel) no depende directamente de una central eléctrica concreta (bajo nivel). Depende de un estándar USB (abstracción). Puedes enchufarlo a un cargador de pared, un power bank o un ordenador. Todos cumplen el estándar USB. Si dependieras directamente de la red eléctrica, ¡no podrías usar un power bank!

DIP es la base de la inyección de dependencias. En lugar de que UserService cree una MySQLDatabase, recibe una Database (interfaz) por constructor. Así puedes cambiar de MySQL a PostgreSQL sin tocar UserService.

❌ Dependencia directa UserService MySQLDatabase Si cambia la BD, cambia UserService 😱 ✅ Dependencia invertida UserService «interface» Database conectar() | query() MySQLDB PostgresDB
Izquierda: dependencia directa a concreto. Derecha: ambos dependen de una abstracción
// ❌ VIOLACIÓN DIP: Alto nivel depende directamente de bajo nivel

// Detalle concreto (bajo nivel)
class MySQLDatabase {
  conectar() { console.log('Conectando a MySQL...'); }
  query(sql) { console.log(`Ejecutando: ${sql}`); }
}

// Módulo de alto nivel (negocio)
class ServicioUsuarios {
  constructor() {
    this.db = new MySQLDatabase(); // ⚠️ Dependencia directa y concreta
    this.db.conectar();
  }

  obtenerUsuarios() {
    return this.db.query('SELECT * FROM usuarios');
  }
}

// ¿Quieres cambiar a PostgreSQL? Tendrías que modificar ServicioUsuarios. 😤
// ¿Y si quieres usar una API mock en tests? Imposible sin cambiar el código.
// ✅ DIP APLICADO: Ambos dependen de la abstracción

// Abstracción (interfaz)
class BaseDeDatos {
  conectar() { throw new Error('implementar'); }
  query(sql) { throw new Error('implementar'); }
}

// Detalles que implementan la abstracción
class MySQLDB extends BaseDeDatos {
  conectar() { console.log('🔵 Conectando a MySQL...'); }
  query(sql) { console.log(`🔵 ${sql}`); }
}

class PostgresDB extends BaseDeDatos {
  conectar() { console.log('🐘 Conectando a PostgreSQL...'); }
  query(sql) { console.log(`🐘 ${sql}`); }
}

// Módulo de alto nivel recibe la abstracción por inyección
class ServicioUsuarios {
  constructor(db) { // 👈 Inyección de dependencia
    this.db = db;
    this.db.conectar();
  }
  obtenerUsuarios() {
    return this.db.query('SELECT * FROM usuarios');
  }
}

// Cambiar de BD es trivial:
// new ServicioUsuarios(new MySQLDB());
// new ServicioUsuarios(new PostgresDB()); // Sin modificar ServicioUsuarios 🎉

🧪 DIP Inyecta y desacopla

// Haz clic en "Ejecutar" para ver el resultado
🤔 ¿Cuál es la forma correcta de aplicar DIP según el principio?
A Hacer que todas las clases sean abstractas
B Los módulos de alto nivel deben depender de abstracciones, no de implementaciones concretas
C Evitar el uso de clases base por completo
D Usar siempre clases estáticas para las dependencias
🏁 Desafío Final

Pon a prueba tu conocimiento

"El código limpio no se escribe por accidente. Se diseña con principios."

📋 Resumen visual de los 5 principios

S

Cada clase, una responsabilidad. Una razón para cambiar.

O

Abierto a extensión, cerrado a modificación.

L

Las subclases deben ser sustituibles por sus clases base.

I

Interfaces pequeñas y específicas. No obligues a implementar lo que no se usa.

D

Depende de abstracciones, no de implementaciones concretas.

🏆 Quiz global: ¿Dominas SOLID?

Responde las 5 preguntas, una por cada principio. ¡Demuestra lo que has aprendido!

Pregunta 1 de 5 Aciertos: 0
0/5
Puntuación final
¡Sigue practicando!

📚 Recursos adicionales

📖 Clean Code

El libro de Robert C. Martin que popularizó SOLID. Imprescindible para todo developer.

🎓 Refactoring Guru

Guía visual con patrones de diseño y principios SOLID explicados con diagramas y ejemplos.

💻 TypeScript SOLID

Ejemplos prácticos de SOLID en TypeScript, ideales para llevar los principios al día a día.

🧪 Testing & SOLID

¿Sabías que seguir SOLID hace tu código mucho más testeable? Descubre la relación.