SOLID es un acrónimo de cinco principios de diseño orientado a objetos introducidos por Robert Martin a principios de la década de 2000. Cada principio aborda un modo de falla específico del diseño orientado a objetos: síntomas que se reconocen en bases de código reales: clases que son imposibles de extender, módulos que se rompen en lugares inesperados cuando se modifican, código que no se puede probar de forma aislada. Los principios SOLID no son reglas para aplicar mecánicamente: son lentes para identificar problemas de diseño y guiar la refactorización. Esta lección cubre cada principio con precisión, muestra la infracción antes de la corrección y explica las implicaciones arquitectónicas.

Los principios SÓLIDOS son medios, no fines. Una clase que viola SRP pero tiene 50 líneas, no tiene colaboradores y nunca cambiará está bien. Una clase que sigue todos los principios mecánicamente pero añade tres capas de indirección a una operación trivial es peor que la violación. Aplicar principios donde resuelvan un problema real.
S — Principio de responsabilidad única

Formulación de Martin: "Una clase debería tener sólo una razón para cambiar". La palabra "razón" es clave: se refiere a un actor o parte interesada cuyos requisitos podrían impulsar un cambio, no a una regla sintáctica de "un método". Una clase que formatea informes Y los guarda en la base de datos tiene dos motivos para cambiar: el equipo de informes (cambios de formato) y el equipo de operaciones (migración de la base de datos). Cuando esos cambios ocurren simultáneamente, entran en conflicto. SRP dice que estas preocupaciones pertenecen a clases separadas.

Haga clic en las líneas resaltadas para comprender la infracción de SRP y su solución.
java
1
// ── VIOLATION: two reasons to change in one class ───────────────
2
class OrderProcessor {
3
  void process(Order order) {
4
    // business logic
5
    order.setStatus(CONFIRMED);
6
    // persistence — reason #2 to change
7
    db.save(order);
8
    // notification — reason #3 to change
9
    emailClient.send(order.getEmail(), buildTemplate(order));
10
  }
11
}
12
13
// ── FIX: each class has one reason to change ─────────────────────
14
class OrderConfirmationService {
15
  OrderConfirmationService(OrderRepository repo, OrderNotificationService notifier) { ... }
16
  void confirm(Order order) {
17
    order.confirm();          // domain logic only
18
    repo.save(order);         // delegates persistence
19
    notifier.sendConfirmation(order); // delegates notification
20
  }
21
}
22
class OrderRepository      { void save(Order o) { ... } }   // DB only
23
class OrderNotificationService { void sendConfirmation(Order o) { ... } } // email only
O — Principio abierto/cerrado

La formulación de Bertrand Meyer (1988), reformulada por Martin: "Las entidades de software deberían estar abiertas a la extensión, pero cerradas a la modificación". Una clase está cerrada para modificaciones cuando su código implementado y probado no necesita cambiar para adaptarse al nuevo comportamiento. Está abierto a la extensión cuando se puede agregar un nuevo comportamiento escribiendo un nuevo código (una nueva subclase, una nueva implementación de estrategia, un nuevo decorador) sin tocar la clase existente. El mecanismo más común: depender de abstracciones (interfaces) e inyectar implementaciones concretas.

Haga clic en las líneas resaltadas para ver OCP violado y corregido
java
1
// ── VIOLATION: every new payment method requires modifying this class
2
class PaymentProcessor {
3
  void process(Payment p) {
4
    if (p.type() == CREDIT_CARD) chargeCard(p);
5
    else if (p.type() == PAYPAL)  chargePayPal(p);
6
    else if (p.type() == CRYPTO)  chargeCrypto(p);  // added later, modified existing class
7
  }
8
}
9
10
// ── FIX: closed for modification, open for extension ────────────
11
interface PaymentGateway { void charge(Payment p); }
12
class CreditCardGateway  implements PaymentGateway { ... }
13
class PayPalGateway       implements PaymentGateway { ... }
14
class CryptoGateway       implements PaymentGateway { ... }  // new method = new class only
15
16
class PaymentProcessor {
17
  private final Map<PaymentType, PaymentGateway> gateways;
18
  void process(Payment p) {
19
    gateways.get(p.type()).charge(p);  // never changes
20
  }
21
}
L - Principio de sustitución de Liskov

Formulación de Barbara Liskov (1987): "Si S es un subtipo de T, entonces los objetos de tipo T pueden ser reemplazados por objetos de tipo S sin alterar ninguna de las propiedades deseables del programa". En términos sencillos: una subclase debe respetar el contrato de su superclase, no sólo las firmas de sus métodos, sino también sus expectativas de comportamiento (condiciones previas, condiciones posteriores, invariantes). La violación clásica de LSP es el problema Cuadrado-Rectángulo: Cuadrado extiende Rectángulo, pero anular setWidth para establecer también la altura viola el contrato del rectángulo de que el ancho y la altura son independientes.

Haga clic en las líneas resaltadas para comprender la infracción de LSP y el diseño correcto.
java
1
// ── VIOLATION: Square breaks Rectangle's behavioral contract ────
2
class Rectangle {
3
  protected int width, height;
4
  void setWidth(int w)  { this.width = w; }
5
  void setHeight(int h) { this.height = h; }
6
  int area() { return width * height; }
7
}
8
class Square extends Rectangle {
9
  @Override void setWidth(int w)  { width = height = w; }  // breaks rectangle contract
10
  @Override void setHeight(int h) { width = height = h; }  // breaks rectangle contract
11
}
12
// Client code breaks when Square is substituted for Rectangle:
13
// rect.setWidth(5); rect.setHeight(4); assert rect.area() == 20; // FAILS for Square
14
15
// ── FIX: model the actual relationship — no inheritance ─────────
16
interface Shape { int area(); }
17
record Rectangle(int width, int height) implements Shape {
18
  public int area() { return width * height; }
19
}
20
record Square(int side) implements Shape {
21
  public int area() { return side * side; }
22
}
I - Principio de segregación de interfaz

Formulación de Martin: "No se debe obligar a los clientes a depender de interfaces que no utilizan". Las interfaces gruesas (interfaces con muchos métodos no relacionados) obligan a los implementadores a proporcionar implementaciones auxiliares de métodos que no necesitan y obligan a los clientes a importar un tipo que incluye capacidades que no utilizan. El ISP dice: prefiera muchas interfaces pequeñas y específicas del cliente a una interfaz grande de propósito general. La presión de diseño del ISP produce naturalmente interfaces de roles: interfaces que describen un rol que desempeña un objeto (imprimible, guardable, auditable) en lugar del menú completo de todo lo que un objeto puede hacer.

Haga clic en las líneas resaltadas para comprender la infracción del ISP y las interfaces de roles
java
1
// ── VIOLATION: fat interface forces irrelevant method stubs ─────
2
interface Worker {
3
  void work();
4
  void takeBreak();   // irrelevant for Robot
5
  void receivePaycheck(); // irrelevant for Robot
6
}
7
class Robot implements Worker {
8
  public void work() { ... }
9
  public void takeBreak() { /* Robot does not rest — forced stub */ }
10
  public void receivePaycheck() { /* Robot is not paid — forced stub */ }
11
}
12
13
// ── FIX: small role interfaces ───────────────────────────────────
14
interface Workable  { void work(); }
15
interface Breakable { void takeBreak(); }
16
interface Payable   { void receivePaycheck(); }
17
18
class Robot      implements Workable { ... }                        // only what it needs
19
class HumanWorker implements Workable, Breakable, Payable { ... }   // full set
20
21
// Scheduler only needs Workable — does not know about breaks or pay:
22
class ProductionScheduler { void schedule(List<Workable> workers) { ... } }
D - Principio de inversión de dependencia

Formulación de Martin: '(A) Los módulos de alto nivel no deberían depender de módulos de bajo nivel. Ambos deberían depender de abstracciones. (B) Las abstracciones no deben depender de los detalles. Los detalles deberían depender de abstracciones. DIP es el principio que subyace a la Arquitectura Cebolla, la Arquitectura Hexagonal y la Arquitectura Limpia, todas las arquitecturas tratadas en el capítulo anterior. También es el principio que hace que la inyección de dependencia sea significativa: DI es el mecanismo; DIP es la razón.

Haga clic en las líneas resaltadas para rastrear la inversión de dependencia desde la infracción hasta su corrección.
java
1
// ── VIOLATION: high-level module depends on low-level detail ────
2
class OrderService {
3
  private final MySqlOrderRepository repo = new MySqlOrderRepository();
4
  void confirm(Long id) {
5
    Order order = repo.findById(id);
6
    order.confirm();
7
    repo.save(order);
8
  }
9
}
10
11
// ── FIX: both depend on the abstraction ──────────────────────────
12
interface OrderRepository { Order findById(Long id); void save(Order o); }
13
14
class OrderService {
15
  private final OrderRepository repo;  // depends on abstraction
16
  OrderService(OrderRepository repo) { this.repo = repo; }  // DI via constructor
17
  void confirm(Long id) { ... }  // unchanged logic
18
}
19
20
// Low-level detail depends on abstraction (not vice versa):
21
class MySqlOrderRepository implements OrderRepository { ... }    // production
22
class InMemoryOrderRepository implements OrderRepository { ... } // tests
SÓLIDO en la práctica: el panorama completo
Referencia rápida: problema que resuelve cada principio y el mecanismo de diseño que prescribe
PrincipioSíntoma que solucionaMecanismoImpacto arquitectónico
SRPLas clases cambian por múltiples razones no relacionadas; fusionar conflictos; pruebas dispersasDividido por actor/motivo del cambioImpulsa la descomposición modular; permite el despliegue independiente
OCPAgregar funciones requiere modificar el código probado; cadenas crecientes si/si noDepender de abstracciones; ampliar añadiendo nuevas implementacionesImpulsa arquitecturas de complementos, patrón de estrategia, puntos de extensión
LSPSubclases que rompen el código de llamada; UnsupportedOperationException en anulacionesHonrar el contrato de comportamiento, no solo la firma.Garantiza la sustituibilidad en jerarquías polimórficas; impulsa la composición sobre la herencia
ISPImplementar métodos irrelevantes; Interfaces gruesas que cambian con demasiada frecuencia.Interfaces pequeñas y específicas de cada función según las necesidades del clienteImpulsa las interfaces de roles, reduce el acoplamiento entre módulos
inmersiónEl código de alto nivel depende de la base de datos/marco; incomprobable sin infraestructuraDefinir abstracciones en paquete de alto nivel; inyectar detallesCimentación de Arquitectura Cebolla/Hexagonal/Limpia; permite dobles de prueba
Aplicaciones incorrectas comunes de SOLID
Haga clic en cada tarjeta: la aplicación mecánica de SOLID causa sus propios problemas
Conclusión clave: Los principios SOLID abordan cinco modos de falla concretos del diseño orientado a objetos. SRP evita el acoplamiento de múltiples actores. OCP evita la modificación del código probado. LSP previene violaciones de contratos de comportamiento en jerarquías. El ISP evita el acoplamiento de la interfaz grasa. DIP evita que el código de alto nivel dependa de detalles de bajo nivel. Aplíquelos donde el síntoma esté presente, no como un modelo universal para cada clase.
Qué sigue: SOLID le indica cómo asignar responsabilidades dentro de una clase y cómo las clases deben depender unas de otras. GRASP proporciona un conjunto complementario de patrones centrados específicamente en la cuestión de qué objeto debería ser responsable de qué. La siguiente lección cubre los nueve patrones GRASP con ejemplos.