🚀 SOLID Principles - Cheat Sheet Conceptual
📋 Decisión Rápida por Contexto
🎯 ARQUITECTURA:
¿Clases con múltiples responsabilidades? → SRP
¿Modificar código para nuevas features? → OCP
¿Subclases rompen comportamiento esperado? → LSP
¿Interfaces obligan a implementar métodos no usados? → ISP
¿Dependencias concretas en business logic? → DIP
💼 REFACTORING:
¿God classes difíciles de testear? → SRP
¿Switch statements que crecen? → OCP
¿instanceof checks en cliente? → LSP
¿UnsupportedOperationException frecuente? → ISP
¿new keyword en services? → DIP
⚡ CODE REVIEW:
¿Cambios en infraestructura afectan business logic? → DIP
¿Agregar funcionalidad requiere tocar múltiples clases? → OCP
¿Tests necesitan mockear muchas dependencias? → SRP + ISP
🧠 CONCEPTOS FUNDAMENTALES
El "Por Qué" de SOLID
SOLID no son reglas arbitrarias - son principios emergentes de décadas observando qué hace que el
software sea costoso de mantener vs fácil de evolucionar.
Trade-offs Universales
Complejidad Inicial vs Flexibilidad Futura: SOLID aumenta complejidad upfront para reducir costo
de cambios futuros
Abstracción vs Simplicidad: Más interfaces y abstracciones, más indirección
Testabilidad vs Performance: Dependency injection puede tener overhead mínimo
🎯 S - SINGLE RESPONSIBILITY PRINCIPLE
Concepto Core
"Una clase debe tener una sola razón para cambiar"
La intuición profunda: Si una clase cambia por múltiples razones, esos cambios pueden entrar en
conflicto entre sí. SRP minimiza el acoplamiento temporal entre features no relacionadas.
java
// ❌ Múltiples razones para cambiar
public class User {
// Razón 1: Cambios en el modelo de datos
public void setEmail(String email) { this.email = email; }
// Razón 2: Cambios en persistencia (DB schema, ORM, etc.)
public void save() {
// SQL queries, conexiones, transacciones
}
// Razón 3: Cambios en formato de salida (JSON, XML, etc.)
public String toJson() { return "..."; }
// Razón 4: Cambios en validaciones de negocio
public boolean isValid() { return email.contains("@"); }
}
// ✅ Una sola razón para cambiar cada clase
public class User { // Solo cambia si el modelo de User cambia
private String email;
public void setEmail(String email) { this.email = email; }
}
public class UserRepository { // Solo cambia si persistencia cambia
public void save(User user) { /* DB logic */ }
}
public class UserValidator { // Solo cambia si validaciones cambian
public boolean isValid(User user) { /* validation logic */ }
}
public class UserSerializer { // Solo cambia si formato cambia
public String toJson(User user) { /* serialization logic */ }
}
Trade-offs Clave:
✅ Facilita testing: Mock solo lo que necesitas
✅ Reduce coupling: Cambios aislados
❌ Más clases: Puede sentirse "over-engineered" inicialmente
❌ Indirección: Más objetos para hacer la misma tarea
Cuándo aplicar intensivamente: Sistemas empresariales, múltiples equipos trabajando en paralelo
Cuándo relajar: Prototipos, scripts simples, casos donde las responsabilidades están naturalmente
acopladas
🎯 O - OPEN/CLOSED PRINCIPLE
Concepto Core
"Abierto para extensión, cerrado para modificación"
La intuición profunda: El costo de cambio crece exponencialmente con el número de lugares que hay
que tocar. OCP localiza cambios en nuevas clases en lugar de modificar existentes.
java
// ❌ Violación: cada nuevo tipo requiere modificar código existente
public class PaymentProcessor {
public void process(String type, double amount) {
if (type.equals("CREDIT_CARD")) {
// CC processing logic
} else if (type.equals("PAYPAL")) {
// PayPal logic
} else if (type.equals("CRYPTO")) { // ← Nueva modificación!
// Crypto logic
}
// Cada nuevo método de pago = modificar esta clase
}
}
// ✅ Extensión sin modificación
public interface PaymentMethod {
void process(double amount);
}
public class CreditCardProcessor implements PaymentMethod {
public void process(double amount) { /* CC logic */ }
}
// Nueva funcionalidad = nueva clase, cero modificaciones
public class CryptoProcessor implements PaymentMethod {
public void process(double amount) { /* Crypto logic */ }
}
public class PaymentService {
private PaymentMethod method;
public PaymentService(PaymentMethod method) {
this.method = method; // Configurado externamente
}
public void processPayment(double amount) {
method.process(amount); // Sin modificar para nuevos tipos
}
}
La Mecánica del OCP:
1. Identificar variación: ¿Qué aspectos cambiarán?
2. Extraer abstracción: Interface/abstract class para la variación
3. Implementar variantes: Cada variante = nueva clase
4. Configurar externamente: Dependency injection, factories
Trade-offs Clave:
✅ Cero riesgo en extensión: Nuevas features no pueden romper existentes
✅ Deployment seguro: Deploy solo lo nuevo
❌ Complejidad inicial: Requires anticipar qué variará
❌ Over-abstraction risk: Abstraer demasiado temprano
Señal para aplicar: Switch statements que crecen, enums que requieren tocar múltiples clases
Señal para evitar: Variación es estática y bien definida, over-engineering temprano
🎯 L - LISKOV SUBSTITUTION PRINCIPLE
Concepto Core
"Subtipos deben ser sustituibles por sus tipos base sin alterar la corrección del programa"
La intuición profunda: LSP garantiza que el polimorfismo funcione matemáticamente. Es el principio
que hace que puedas escribir código genérico sin preocuparte por la implementación específica.
java
// ❌ Violación: Square rompe las expectativas de Rectangle
public class Rectangle {
protected int width, height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return width * height; }
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = this.height = width; // ← Fortalece precondición!
}
@Override
public void setHeight(int height) {
this.width = this.height = height; // ← Cambia comportamiento!
}
}
// Cliente se rompe con Square
public void expandRectangle(Rectangle rect) {
rect.setWidth(5);
rect.setHeight(4);
assert rect.getArea() == 20; // ❌ Falla con Square (area = 16)
}
// ✅ LSP-compliant: cada tipo mantiene su contrato
public interface Shape {
int getArea();
// Contrato: siempre devuelve área >= 0
}
public class Rectangle implements Shape {
private final int width, height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public int getArea() { return width * height; }
}
public class Square implements Shape {
private final int side;
public Square(int side) { this.side = side; }
public int getArea() { return side * side; }
}
// Cliente funciona con cualquier Shape
public void processShapes(List<Shape> shapes) {
for (Shape shape : shapes) {
int area = shape.getArea(); // ✅ Siempre funciona
System.out.println("Area: " + area);
}
}
Las Reglas Matemáticas del LSP:
1. Precondiciones: Subclases no pueden ser más estrictas
2. Postcondiciones: Subclases no pueden ser más relajadas
3. Invariantes: Subclases deben preservar propiedades de la clase base
4. Historia: Subclases no pueden modificar comportamiento inmutable
Trade-offs Clave:
✅ Polimorfismo confiable: Código genérico que realmente funciona
✅ Composabilidad: Componentes se pueden combinar sin sorpresas
❌ Restricciones de diseño: Limita cómo puedes diseñar jerarquías
❌ Complejidad conceptual: Requiere pensar en contratos formales
Cuándo es crítico: APIs públicas, frameworks, bibliotecas reutilizables
Cuándo puede ser relajado: Código interno bien conocido, prototipado rápido
🎯 I - INTERFACE SEGREGATION PRINCIPLE
Concepto Core
"Los clientes no deben depender de interfaces que no usan"
La intuición profunda: Dependencias innecesarias crean acoplamiento fantasma - cambios en código
que no usas pueden romperte. ISP minimiza el blast radius de cambios.
java
// ❌ Interface monolítica crea dependencias fantasma
public interface DocumentProcessor {
// Grupo 1: Operaciones de lectura
String readContent(String id);
// Grupo 2: Operaciones de escritura
void writeContent(String id, String content);
// Grupo 3: Operaciones de conversión
byte[] convertToPDF(String id);
// Grupo 4: Operaciones de sharing
void shareWithUser(String id, String userId);
}
// Cliente read-only forzado a conocer sobre sharing, PDF, etc.
public class DocumentViewer implements DocumentProcessor {
public String readContent(String id) { /* implementación */ }
// Dependencias fantasma - cambios aquí pueden afectar DocumentViewer
public void writeContent(String id, String content) {
throw new UnsupportedOperationException();
}
public byte[] convertToPDF(String id) {
throw new UnsupportedOperationException();
}
public void shareWithUser(String id, String userId) {
throw new UnsupportedOperationException();
}
}
// ✅ Interfaces segregadas eliminan dependencias fantasma
public interface DocumentReader {
String readContent(String id);
// Solo cambios en lectura afectan a readers
}
public interface DocumentWriter {
void writeContent(String id, String content);
// Solo cambios en escritura afectan a writers
}
public interface DocumentConverter {
byte[] convertToPDF(String id);
// Solo cambios en conversión afectan a converters
}
// Cliente depende solo de lo que usa
public class DocumentViewer implements DocumentReader {
public String readContent(String id) { /* implementación */ }
// Cambios en DocumentWriter/DocumentConverter no afectan esto
}
El Mecanismo de Segregación:
1. Agrupar por cliente: ¿Qué métodos usa cada tipo de cliente?
2. Extraer interfaces focused: Una interface por grupo cohesivo
3. Componer cuando necesario: Cliente usa múltiples interfaces si necesita
Trade-offs Clave:
✅ Blast radius reducido: Cambios afectan menos clientes
✅ Testing simplificado: Mock solo interfaces relevantes
❌ Más interfaces: Proliferación de abstracciones
❌ Complejidad de composición: Clientes que necesitan múltiples interfaces
Métrica útil: Si >30% de una interface no se usa por un cliente típico, considera segregar
Red flag: UnsupportedOperationException frecuente en implementaciones
🎯 D - DEPENDENCY INVERSION PRINCIPLE
Concepto Core
"Módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de
abstracciones."
La intuición profunda: DIP invierte el flujo de control tradicional. En lugar de business logic
dependiendo de infraestructura, ambos dependen de contratos. Esto hace business logic testeable
independientemente.
java
// ❌ Business logic acoplado a infraestructura
public class OrderService {
// Dependencias concretas = acoplamiento fuerte
private MySQLOrderRepository repository = new MySQLOrderRepository();
private SmtpEmailService emailService = new SmtpEmailService();
public void processOrder(Order order) {
// Business logic mezclado con detalles de implementación
repository.save(order); // ¿Qué si cambiamos a MongoDB?
emailService.sendConfirmation(order); // ¿Qué si cambiamos a SendGrid?
// Testing requiere MySQL + SMTP reales
}
}
// ✅ Dependencies invertidas - business logic independiente
public class OrderService {
// Dependencias abstractas = flexibilidad
private final OrderRepository repository;
private final EmailService emailService;
// Dependency injection
public OrderService(OrderRepository repository, EmailService emailService) {
this.repository = repository;
this.emailService = emailService;
}
public void processOrder(Order order) {
// Business logic pura - sin detalles de implementación
repository.save(order);
emailService.sendConfirmation(order);
// Testing con mocks, cero infraestructura externa
}
}
// Implementaciones concretas dependen de abstracciones
public class MySQLOrderRepository implements OrderRepository {
public void save(Order order) { /* MySQL-specific logic */ }
}
public class MongoOrderRepository implements OrderRepository {
public void save(Order order) { /* MongoDB-specific logic */ }
}
El Flujo de Inversión:
1. Tradicional: Business Logic → Infrastructure → External Systems
2. Invertido: Business Logic → Abstractions ← Infrastructure Implementations
Trade-offs Clave:
✅ Testabilidad independiente: Unit tests sin infraestructura externa
✅ Flexibilidad tecnológica: Cambiar DB/email/etc sin tocar business logic
✅ Deployment independiente: Business logic puede ejecutar sin infraestructura completa
❌ Indirección: Una capa extra de abstracción
❌ Setup complexity: Requires DI container o manual wiring
Señal fuerte para DIP: new keyword en business logic classes
Contexto donde es crítico: Microservices, testing, tecnologías que cambian frecuentemente
⚖️ TRADE-OFFS Y DECISIONES ARQUITECTÓNICAS
El Espectro de SOLID
Simple/Rápido ←→ Mantenible/Escalable
Script/Prototipo MVP/Startup Empresa/Long-term
SOLID = 20% SOLID = 60% SOLID = 90%
Aplicación Contextual
Contexto SRP OCP LSP ISP DIP Razón
Prototipo ⚠️ ❌ ❌ ❌ ⚠️ Velocidad > estructura
MVP ✅ ⚠️ ⚠️ ⚠️ ✅ Separar business logic
Startup ✅ ⚠️ ✅ ⚠️ ✅ Preparar para pivots
Enterprise ✅ ✅ ✅ ✅ ✅ Múltiples equipos/años
Library/API ✅ ✅ ✅ ✅ ⚠️ Usabilidad externa
Indicadores para Aplicar SOLID
Aplicar agresivamente cuando:
Múltiples equipos trabajando en el mismo codebase
Ciclo de vida del software >2 años
Requirements que cambian frecuentemente
Alta cobertura de testing requerida
Integraciones con sistemas externos que cambian
Relajar cuando:
Scripts one-off o herramientas temporales
Prototipos o proof-of-concepts
Team muy pequeño (<3 developers)
Dominio del problema muy estable
Performance extremadamente crítico
Señales de Que Necesitas Más SOLID
Cambios simples requieren tocar muchos archivos
Testing requiere mucho setup de infraestructura
Developers nuevos tardan semanas en ser productivos
Bug fixes introducen bugs en areas no relacionadas
Deploy de features require coordinar múltiples teams
Señales de Over-Application de SOLID
Más interfaces que classes concretas
Developers quejándose de "too much indirection"
Features simples requieren crear 5+ clases
Código difícil de debuggear por tantas abstracciones
Performance issues por layers innecesarios
🎯 SOLID EN PRÁCTICA
Orden de Implementación Recomendado
1. DIP primero → Establece inyección de dependencias
2. SRP segundo → Separa responsabilidades ahora que dependencies están claras
3. ISP tercero → Segrega interfaces cuando ya tienes boundaries claros
4. OCP cuarto → Hace extensible cuando structure es stable
5. LSP último → Refina jerarquías cuando everything else está in place
Patterns que Facilitan SOLID
Strategy Pattern → OCP en algorithms
Factory Pattern → OCP en object creation
Adapter Pattern → LSP para legacy integration
Facade Pattern → ISP para complex subsystems
Dependency Injection → DIP en infraestructura
Métricas de Éxito
Tiempo para implement new feature (debería decrecer)
Número de files modificados por feature (debería decrecer)
Test coverage sin external dependencies (debería incrementar)
Time to onboard new developers (debería decrecer)
Frequency of bugs in unrelated areas (debería decrecer)
🚨 RED FLAGS Y ANTI-PATRONES
Violaciones Comunes por Principio
java
// SRP violation
public class UserManagerServiceUtilHelper { } // God class name
// OCP violation
public enum PaymentType { CC, PAYPAL, BITCOIN } // Growing enum
// LSP violation
@Override
public void fly() { throw new UnsupportedOperationException("Penguins can't fly"); }
// ISP violation
public interface Animal { void fly(); void swim(); void run(); } // Fat interface
// DIP violation
public class OrderService {
private EmailService email = new GmailService(); // Concrete dependency
}
Debugging SOLID Issues
Compilation breaks cuando adds new type → OCP violation
Can't test business logic without database → DIP violation
Change in unrelated class breaks your tests → ISP violation
Subclass behaves unexpectedly in existing code → LSP violation
Single class changes for multiple different reasons → SRP violation
💡 Philosophy: SOLID principles son guidelines emergentes de observar qué hace software
maintainable. No son dogma - son herramientas. Aplica pragmáticamente según tu context, pero
entiende el costo cuando los ignoras.