Principios SOLID en Java
Aplica los principios SOLID — SRP, OCP, LSP, ISP, DIP — al diseño en Java.
SOLID es un conjunto de cinco principios de diseño orientado a objetos — popularizados por Robert C. Martin — que mantienen el código Java fácil de cambiar, probar y extender a medida que crece. No son reglas de sintaxis que el compilador aplica; son directrices sobre dónde trazar los límites entre clases para que un cambio no se propague por todo el código. El acrónimo corresponde a Single Responsibility (Responsabilidad Única), Open/Closed (Abierto/Cerrado), Liskov Substitution (Sustitución de Liskov), Interface Segregation (Segregación de Interfaces) y Dependency Inversion (Inversión de Dependencias).
Estos principios se basan en los fundamentos orientados a objetos: verás interfaces, herencia y polimorfismo a lo largo de todo el capítulo. Si alguno de estos conceptos no está claro, revísalos primero — SOLID es principalmente buen juicio sobre dónde aplicarlos.
Los cinco principios en un vistazo
Cada letra apunta a un tipo específico de problema de diseño. Ten esta tabla a mano mientras lees el resto del capítulo:
| Letra | Principio | Objetivo en una línea |
|---|---|---|
| S | Single Responsibility | Una clase debe tener una sola razón para cambiar |
| O | Open/Closed | Abierto para la extensión, cerrado para la modificación |
| L | Liskov Substitution | Los subtipos deben poder usarse donde se use su tipo base |
| I | Interface Segregation | Muchas interfaces pequeñas son mejores que una grande |
| D | Dependency Inversion | Depender de abstracciones, no de clases concretas |
Los principios se refuerzan mutuamente. En código bien estructurado rara vez se aplica solo uno — una interfaz pequeña (ISP) de la que depende el código de alto nivel (DIP) es exactamente lo que permite añadir una nueva implementación (OCP) sin tocar quien la consume.
S — Principio de Responsabilidad Única
Una clase debe hacer una cosa y tener una sola razón para cambiar. Cuando preocupaciones no relacionadas — reglas de negocio y entrega de mensajes, por ejemplo — comparten una clase, un cambio en cualquiera de ellas te obliga a volver a probar ambas. Separarlas aísla el cambio.
// Mixes WHEN to alert with HOW to deliver -- two reasons to change.
class BadAlertService {
void raise(String user, int errors) {
if (errors > 0) {
// ...build an email, open an SMTP connection, send...
}
}
}
// One responsibility: deciding when to alert. Delivery lives elsewhere.
class AlertService {
private final Notifier notifier;
AlertService(Notifier notifier) { this.notifier = notifier; }
void raise(String user, int errors) {
if (errors > 0) notifier.send(user, errors + " error(s) detected");
}
}O — Principio Abierto/Cerrado
Las entidades de software deben estar abiertas para la extensión pero cerradas para la modificación. Debes poder añadir nuevo comportamiento escribiendo código nuevo, no editando — y arriesgando — código que ya funciona. En Java el mecanismo habitual es una interfaz estable más nuevas implementaciones.
interface Notifier { void send(String to, String message); }
class EmailNotifier implements Notifier { /* ... */ }
class SmsNotifier implements Notifier { /* ... */ } // new feature = new class
// AlertService never changes when a new channel appears.Añadir notificaciones push más adelante significa escribir PushNotifier implements Notifier — AlertService no se toca, por lo que no necesita revisión ni supone riesgo de regresión.
L — Principio de Sustitución de Liskov
Si S es un subtipo de T, los objetos de tipo T pueden ser reemplazados por objetos de tipo S sin que el programa se rompa. Una subclase debe respetar el contrato de su clase padre — mismas expectativas, sin excepciones sorpresivas, sin precondiciones más estrictas.
abstract class Shape { abstract double area(); }
class Rectangle extends Shape { /* area() = w * h */ }
class Circle extends Shape { /* area() = PI * r * r */ }
// Works for ANY Shape, present or future, without inspecting the concrete type.
double totalArea(List<Shape> shapes) {
return shapes.stream().mapToDouble(Shape::area).sum();
}La violación clásica es Square extends Rectangle: si establecer el ancho también modifica la altura, el código escrito para un Rectangle falla al recibir un Square. La solución es modelarlos como hermanos bajo Shape, no como una relación padre-hijo. (Consulta clases abstractas para ver la base Shape utilizada aquí.)
I — Principio de Segregación de Interfaces
Los clientes no deben verse obligados a depender de métodos que no usan. Prefiere varias interfaces pequeñas y enfocadas en lugar de una grande — de lo contrario, un implementador se ve arrastrado a proporcionar stubs de métodos que no puede cumplir.
// Fat interface: a read-only source is forced to implement write().
interface Storage { String read(); void write(String data); }
// Segregated: implement only what you can honor.
interface Readable { String read(); }
interface Writable { void write(String data); }
class ConfigFile implements Readable { // no empty write() stub
public String read() { return "mode=prod"; }
}D — Principio de Inversión de Dependencias
Los módulos de alto nivel no deben depender de los de bajo nivel; ambos deben depender de abstracciones. En la práctica: programa contra interfaces e inyecta la implementación concreta (la inyección por constructor es la forma más simple). Esto es lo que hace que los demás principios sean rentables — y lo que hace que una clase sea comprobable, ya que puedes inyectar un objeto falso.
// AlertService depends on the Notifier interface, not EmailNotifier.
AlertService alerts = new AlertService(new EmailNotifier());
// In a test, inject a fake Notifier and assert on what it recorded.Un ejemplo completo: los cinco principios en un programa
Este programa conecta todos los principios — un único AlertService (SRP) habla con un Notifier inyectado (DIP), intercambia entre un EmailNotifier y un SmsNotifier sin cambiar (OCP), lee un ConfigFile de solo lectura mediante Readable (ISP), y suma áreas de subtipos de Shape de forma uniforme (LSP). Verifica sus propios resultados para que puedas ver cada principio en acción.
Lo que se puede observar al ejecutarlo:
email sent: [EMAIL -> alice: 3 error(s) detected]contiene solo una entrada —bobtenía cero errores, por lo queraiseno envió nada.AlertServicetiene la responsabilidad única de decidir cuándo alertar (SRP); nunca construye el cuerpo del mensaje ni abre una conexión.- La misma clase
AlertServicegestionó tanto unEmailNotifiercomo unSmsNotifierporque la dependencia se le pasó a través del constructor (DIP). La lógica de alertas de alto nivel depende únicamente de la interfazNotifier, nunca de un emisor concreto. OCP check : ... unchanged = trueconfirma que ambos objetos de alerta son la misma claseAlertService: añadir soporte para SMS implicó escribir un nuevoSmsNotifier, con cero ediciones aAlertService— abierto para la extensión, cerrado para la modificación.ISP check : is Writable? falsemuestra queConfigFileimplementa soloReadable. Gracias a que las interfaces están segregadas, la fuente de solo lectura nunca tuvo que proporcionar un stubwritesin sentido.LSP area : 9.142es la suma de un rectángulo 2×3 (6.0) y un círculo de radio 1 (≈3.142).totalArearecorrió referencias deShapey llamó aarea()sin comprobar de qué subtipo se trataba — cada subtipo era sustituible por su base (LSP).