SOLID es el acrónimo de cinco principios de diseño orientado a objetos popularizados por Robert C. Martin. Aunque suelen presentarse como reglas de programación, su verdadero valor es arquitectónico: cuando se aplican con criterio, producen sistemas cuyos componentes son independientes, sustituibles y resistentes al cambio. Cada principio ataca una forma concreta de rigidez o fragilidad. En esta lección estudiaremos los cinco, viendo en cada caso un ejemplo de violación en Java, su corrección y, sobre todo, qué implica el principio cuando lo elevamos del nivel de clase al nivel de módulos y servicios.
Contenido
- Qué significa SOLID y por qué es arquitectónico
- S — Principio de Responsabilidad Única (SRP)
- O — Principio de Abierto/Cerrado (OCP)
- L — Principio de Sustitución de Liskov (LSP)
- I — Principio de Segregación de Interfaces (ISP)
- D — Principio de Inversión de Dependencias (DIP)
- SOLID a nivel de arquitectura
- Errores comunes y consejos
- Ejercicios
- Conclusión
- Qué significa SOLID y por qué es arquitectónico
| Letra | Principio | Idea central |
|---|---|---|
| S | Responsabilidad Única | Una clase debe tener una sola razón para cambiar |
| O | Abierto/Cerrado | Abierto a extensión, cerrado a modificación |
| L | Sustitución de Liskov | Los subtipos deben poder reemplazar a sus tipos base |
| I | Segregación de Interfaces | Interfaces pequeñas y específicas, no monolíticas |
| D | Inversión de Dependencias | Depende de abstracciones, no de concreciones |
El hilo conductor de los cinco es gestionar las dependencias y las razones de cambio. A nivel de clase mejoran el código; a nivel de arquitectura determinan cómo se dibujan las fronteras entre módulos y en qué dirección apuntan las dependencias.
- S — Principio de Responsabilidad Única (SRP)
Una clase debe tener una única razón para cambiar.
"Razón para cambiar" significa un único actor o aspecto del negocio responsable de pedir modificaciones. Si una clase atiende a varios actores, los cambios de uno pueden romper lo que necesita otro.
// VIOLACIÓN: tres razones de cambio en una clase
public class Empleado {
public double calcularNomina() { /* lógica de RRHH */ return 0; }
public void guardar() { /* lógica de persistencia (BD) */ }
public String generarInformeHoras() { /* lógica de reporting */ return ""; }
}Explicación del problema: calcularNomina cambia si cambian las reglas de RRHH; guardar cambia si cambia el esquema de la base de datos; generarInformeHoras cambia si cambia el formato de los informes. Tres actores distintos, una sola clase: cualquier cambio arriesga a romper a los demás.
// CORRECCIÓN: una responsabilidad por clase
public class Empleado {
private double salarioBase;
private double horasTrabajadas;
// solo datos y reglas propias del empleado
}
public class CalculadoraNomina {
public double calcular(Empleado e) { /* reglas de RRHH */ return 0; }
}
public class RepositorioEmpleados {
public void guardar(Empleado e) { /* persistencia */ }
}
public class GeneradorInformeHoras {
public String generar(Empleado e) { /* reporting */ return ""; }
}Cada clase tiene ahora un único motivo para cambiar y un único responsable de negocio detrás.
- O — Principio de Abierto/Cerrado (OCP)
Las entidades deben estar abiertas a la extensión pero cerradas a la modificación.
Deberíamos poder añadir comportamiento nuevo sin tocar el código ya probado y en producción. La herramienta habitual es el polimorfismo.
// VIOLACIÓN: cada nuevo tipo obliga a modificar el método
public class CalculadoraArea {
public double calcular(Object figura) {
if (figura instanceof Circulo) {
Circulo c = (Circulo) figura;
return Math.PI * c.radio * c.radio;
} else if (figura instanceof Rectangulo) {
Rectangulo r = (Rectangulo) figura;
return r.ancho * r.alto;
}
return 0;
}
}Explicación del problema: añadir un Triangulo exige modificar calcular, recompilar y volver a probar todo. El método crece sin límite y concentra el riesgo.
// CORRECCIÓN: extensión por nuevas clases, sin modificar las existentes
public interface Figura {
double area();
}
public class Circulo implements Figura {
private final double radio;
public Circulo(double radio) { this.radio = radio; }
public double area() { return Math.PI * radio * radio; }
}
public class Rectangulo implements Figura {
private final double ancho, alto;
public Rectangulo(double ancho, double alto) { this.ancho = ancho; this.alto = alto; }
public double area() { return ancho * alto; }
}
// Para añadir un Triangulo: nueva clase que implementa Figura. Nada más cambia.
- L — Principio de Sustitución de Liskov (LSP)
Si S es un subtipo de T, los objetos de tipo T deben poder sustituirse por objetos de tipo S sin alterar la corrección del programa.
Heredar no basta: la subclase debe respetar el contrato de la superclase. El ejemplo clásico es el rectángulo-cuadrado.
// VIOLACIÓN: Cuadrado rompe el contrato de Rectangulo
public class Rectangulo {
protected int ancho, alto;
public void setAncho(int a) { this.ancho = a; }
public void setAlto(int a) { this.alto = a; }
public int area() { return ancho * alto; }
}
public class Cuadrado extends Rectangulo {
public void setAncho(int a) { this.ancho = a; this.alto = a; }
public void setAlto(int a) { this.ancho = a; this.alto = a; }
}Explicación del problema: un código que espera un Rectangulo asume que al fijar ancho=5 y alto=4 el área es 20. Con un Cuadrado el área sería 16. La subclase viola la expectativa del cliente; el polimorfismo se vuelve traicionero.
// CORRECCIÓN: modelar lo que comparten, no forzar la herencia
public interface Figura {
int area();
}
public final class Rectangulo implements Figura {
private final int ancho, alto;
public Rectangulo(int ancho, int alto) { this.ancho = ancho; this.alto = alto; }
public int area() { return ancho * alto; }
}
public final class Cuadrado implements Figura {
private final int lado;
public Cuadrado(int lado) { this.lado = lado; }
public int area() { return lado * lado; }
}Reglas prácticas para no romper LSP: no fortalecer precondiciones, no debilitar postcondiciones, no lanzar excepciones nuevas inesperadas y no cambiar el significado del contrato.
- I — Principio de Segregación de Interfaces (ISP)
Ningún cliente debería verse obligado a depender de métodos que no usa.
Las interfaces "gordas" obligan a las implementaciones a cargar con métodos irrelevantes.
// VIOLACIÓN: interfaz monolítica
public interface Trabajador {
void trabajar();
void comer();
void cobrarSalario();
}
// Un robot tiene que implementar comer() sin sentido
public class Robot implements Trabajador {
public void trabajar() { /* ok */ }
public void comer() { throw new UnsupportedOperationException(); }
public void cobrarSalario() { throw new UnsupportedOperationException(); }
}Explicación del problema: Robot se ve forzado a implementar comer() y cobrarSalario() que no le aplican, lo que produce métodos vacíos o que lanzan excepciones (lo que de paso viola LSP).
// CORRECCIÓN: interfaces pequeñas y enfocadas
public interface Trabajable { void trabajar(); }
public interface Alimentable { void comer(); }
public interface Asalariado { void cobrarSalario(); }
public class Humano implements Trabajable, Alimentable, Asalariado {
public void trabajar() {}
public void comer() {}
public void cobrarSalario() {}
}
public class Robot implements Trabajable {
public void trabajar() {}
}Cada clase implementa solo lo que de verdad necesita.
- D — Principio de Inversión de Dependencias (DIP)
Los módulos de alto nivel no deben depender de los de bajo nivel; ambos deben depender de abstracciones. Las abstracciones no deben depender de los detalles.
Es el principio más profundamente arquitectónico: invierte la dirección natural de las dependencias.
// VIOLACIÓN: la lógica de negocio depende de un detalle concreto
public class ServicioFacturacion {
private final RepositorioMySql repo = new RepositorioMySql(); // detalle concreto
public void facturar(Factura f) {
repo.guardar(f);
}
}Explicación del problema: la clase de alto nivel (ServicioFacturacion) depende directamente de un detalle de bajo nivel (RepositorioMySql). Cambiar de MySql a PostgreSQL, o testear sin BD, obliga a tocar el negocio.
// CORRECCIÓN: ambos dependen de una abstracción definida por el negocio
public interface RepositorioFacturas { // abstracción (la define el dominio)
void guardar(Factura f);
}
public class ServicioFacturacion { // alto nivel
private final RepositorioFacturas repo;
public ServicioFacturacion(RepositorioFacturas repo) { this.repo = repo; }
public void facturar(Factura f) { repo.guardar(f); }
}
public class RepositorioMySql implements RepositorioFacturas { // bajo nivel (detalle)
public void guardar(Factura f) { /* JDBC */ }
}Explicación de la mejora: la interfaz RepositorioFacturas pertenece conceptualmente a la capa de negocio. El detalle de MySql la implementa. Así la flecha de dependencia apunta hacia el dominio, no hacia la infraestructura: esto es la inversión.
- SOLID a nivel de arquitectura
Al subir de la clase al sistema, cada principio adquiere una lectura más amplia:
| Principio | A nivel de clase | A nivel de arquitectura |
|---|---|---|
| SRP | Una razón para cambiar por clase | Un servicio/módulo por capacidad de negocio (límites de contexto) |
| OCP | Polimorfismo para extender | Plugins, extensiones y feature flags sin redeployar el núcleo |
| LSP | Subtipos sustituibles | Implementaciones intercambiables de un contrato (p. ej. proveedores) |
| ISP | Interfaces pequeñas | APIs y contratos de servicio cohesionados; evitar APIs "gordas" |
| DIP | Inyección de dependencias | Arquitectura hexagonal/limpia: dominio independiente de la infraestructura |
El DIP es la base de las arquitecturas limpias y hexagonales, donde el dominio define puertos (interfaces) y la infraestructura aporta adaptadores. El siguiente diagrama lo ilustra:
graph TD
UI[Adaptador Web] --> P1[Puerto entrada]
P1 --> D[Dominio / Casos de uso]
D --> P2[Puerto salida: RepositorioFacturas]
DB[Adaptador MySql] -.implementa.-> P2Explicación del diagrama: el dominio está en el centro y no apunta a ningún detalle. Los adaptadores (web, base de datos) dependen del dominio a través de puertos. La regla de dependencia siempre apunta hacia dentro.
Errores Comunes y Consejos
- Sobreaplicar SRP hasta crear una explosión de microclases anémicas. El criterio es "una razón de cambio", no "una línea por clase".
- Confundir OCP con prohibir todo cambio. Sí se modifica para corregir errores; lo que se evita es modificar para añadir variantes previsibles.
- Heredar por reutilizar código en lugar de por una relación "es-un" real: principal fuente de violaciones de LSP. Prefiere composición.
- Crear una interfaz por clase mecánicamente (DIP mal entendido). El DIP pide abstracciones donde hay variación o frontera, no en todas partes.
- Colocar las interfaces (puertos) en la capa de infraestructura. Para que el DIP invierta de verdad, la abstracción debe pertenecer al lado que la consume (el dominio).
- Consejo: SOLID son guías, no leyes físicas. El objetivo final es código mantenible; si un principio te lleva a más complejidad sin beneficio, reconsidéralo.
Ejercicios
Ejercicio 1 (SRP). Esta clase viola SRP. Indica las razones de cambio y propón una separación:
public class GestorUsuario {
public void registrar(Usuario u) { /* valida y guarda en BD */ }
public void enviarBienvenida(Usuario u) { /* envía email */ }
}Ejercicio 2 (OCP). Refactoriza para cumplir Abierto/Cerrado:
public class CalculadoraPrecio {
public double precio(String tipoCliente, double base) {
if (tipoCliente.equals("VIP")) return base * 0.8;
if (tipoCliente.equals("NORMAL")) return base;
return base;
}
}Ejercicio 3 (DIP). ¿Qué principio viola este código y cómo lo corriges?
public class GeneradorReporte {
private final ImpresoraEpson impresora = new ImpresoraEpson();
public void imprimir(String r) { impresora.imprimir(r); }
}Soluciones
Solución 1. Hay dos razones de cambio: el registro/persistencia y la comunicación por email. Separamos:
public class ServicioRegistro {
private final RepositorioUsuarios repo;
public ServicioRegistro(RepositorioUsuarios repo) { this.repo = repo; }
public void registrar(Usuario u) { /* valida */ repo.guardar(u); }
}
public class ServicioBienvenida {
private final PasarelaEmail email;
public ServicioBienvenida(PasarelaEmail email) { this.email = email; }
public void enviar(Usuario u) { email.enviar(u.getEmail(), "Bienvenido"); }
}Solución 2. Modelamos el tipo de cliente como una estrategia polimórfica:
public interface PoliticaPrecio {
double aplicar(double base);
}
public class PrecioVip implements PoliticaPrecio {
public double aplicar(double base) { return base * 0.8; }
}
public class PrecioNormal implements PoliticaPrecio {
public double aplicar(double base) { return base; }
}
public class CalculadoraPrecio {
public double precio(PoliticaPrecio politica, double base) {
return politica.aplicar(base);
}
}Un nuevo tipo de cliente es una nueva clase, sin tocar la calculadora.
Solución 3. Viola el DIP (y dificulta el testeo): depende de la concreción ImpresoraEpson. Corrección:
public interface Impresora { void imprimir(String r); }
public class GeneradorReporte {
private final Impresora impresora;
public GeneradorReporte(Impresora impresora) { this.impresora = impresora; }
public void imprimir(String r) { impresora.imprimir(r); }
}
public class ImpresoraEpson implements Impresora {
public void imprimir(String r) { /* ... */ }
}Conclusión
Los principios SOLID son cinco respuestas concretas a la pregunta de cómo gestionar dependencias y razones de cambio. SRP separa responsabilidades, OCP permite extender sin romper, LSP garantiza polimorfismo seguro, ISP mantiene contratos enfocados y DIP invierte las dependencias para proteger el dominio. Llevados al plano arquitectónico, sustentan las arquitecturas limpias y hexagonales. Conviene recordar que son guías al servicio de la mantenibilidad, no fines en sí mismos. En la siguiente lección complementaremos SOLID con otra familia de principios más pragmáticos —DRY, KISS y YAGNI— que regulan el equilibrio entre rigor y simplicidad.
Curso de Arquitectura de Aplicaciones
Módulo 1: Fundamentos de la Arquitectura de Aplicaciones
- ¿Qué es la Arquitectura de Aplicaciones?
- El Rol del Arquitecto de Software
- Atributos de Calidad y Requisitos No Funcionales
- Decisiones Arquitectónicas y Compromisos (Trade-offs)
- Documentación de Arquitectura: Vistas y el Modelo C4
Módulo 2: Principios y Tácticas de Diseño
- Acoplamiento, Cohesión y Separación de Responsabilidades
- Principios SOLID Aplicados a la Arquitectura
- DRY, KISS, YAGNI y Otros Principios de Diseño
- Tácticas Arquitectónicas para los Atributos de Calidad
- Gestión de la Deuda Técnica
Módulo 3: Estilos y Patrones Arquitectónicos
- Arquitectura Monolítica
- Arquitectura en Capas (N-Tier)
- Arquitectura Cliente-Servidor
- Arquitectura Hexagonal (Puertos y Adaptadores)
- Arquitectura Limpia y Cebolla (Clean & Onion)
Módulo 4: Arquitecturas Distribuidas y Microservicios
- Introducción a los Sistemas Distribuidos
- Arquitectura de Microservicios
- Descomposición de Servicios y Bounded Contexts
- API Gateway, Service Discovery y Comunicación entre Servicios
- Patrones de Resiliencia: Circuit Breaker, Retry y Bulkhead
- El Teorema CAP y la Consistencia de Datos
Módulo 5: Arquitecturas Dirigidas por Eventos y Mensajería
- Fundamentos de la Arquitectura Orientada a Eventos
- Mensajería Asíncrona: Colas y Brokers
- Patrones de Eventos: Event Sourcing y CQRS
- Gestión de Transacciones Distribuidas: Patrón Saga
- Streaming de Datos en Tiempo Real
Módulo 6: Diseño Dirigido por el Dominio (DDD)
- Conceptos Fundamentales del DDD
- Diseño Estratégico: Bounded Contexts y Lenguaje Ubicuo
- Diseño Táctico: Entidades, Agregados y Repositorios
- Mapeo de Contextos (Context Mapping)
Módulo 7: Datos y Persistencia
- Estrategias de Persistencia: SQL vs NoSQL
- Patrones de Acceso a Datos: Repository, Unit of Work y DAO
- Base de Datos por Servicio y Gestión de Datos Distribuidos
- Caché y Estrategias de Invalidación
Módulo 8: Arquitectura en la Nube y Despliegue
- Fundamentos del Cloud Computing (IaaS, PaaS, SaaS)
- Contenedores y Orquestación con Docker y Kubernetes
- Arquitectura Serverless
- Patrones de Diseño Cloud-Native
- Infraestructura como Código (IaC)
Módulo 9: Calidad, Seguridad y Observabilidad
- Escalabilidad: Horizontal vs Vertical y Balanceo de Carga
- Alta Disponibilidad y Tolerancia a Fallos
- Seguridad por Diseño y Autenticación/Autorización
- Observabilidad: Logging, Métricas y Trazabilidad
- Rendimiento y Pruebas de Carga
