W3docs

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:

LetraPrincipioObjetivo en una línea
SSingle ResponsibilityUna clase debe tener una sola razón para cambiar
OOpen/ClosedAbierto para la extensión, cerrado para la modificación
LLiskov SubstitutionLos subtipos deben poder usarse donde se use su tipo base
IInterface SegregationMuchas interfaces pequeñas son mejores que una grande
DDependency InversionDepender 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 NotifierAlertService 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.

java— editable, runs on the server

Lo que se puede observar al ejecutarlo:

  • email sent: [EMAIL -> alice: 3 error(s) detected] contiene solo una entrada — bob tenía cero errores, por lo que raise no envió nada. AlertService tiene la responsabilidad única de decidir cuándo alertar (SRP); nunca construye el cuerpo del mensaje ni abre una conexión.
  • La misma clase AlertService gestionó tanto un EmailNotifier como un SmsNotifier porque la dependencia se le pasó a través del constructor (DIP). La lógica de alertas de alto nivel depende únicamente de la interfaz Notifier, nunca de un emisor concreto.
  • OCP check : ... unchanged = true confirma que ambos objetos de alerta son la misma clase AlertService: añadir soporte para SMS implicó escribir un nuevo SmsNotifier, con cero ediciones a AlertService — abierto para la extensión, cerrado para la modificación.
  • ISP check : is Writable? false muestra que ConfigFile implementa solo Readable. Gracias a que las interfaces están segregadas, la fuente de solo lectura nunca tuvo que proporcionar un stub write sin sentido.
  • LSP area : 9.142 es la suma de un rectángulo 2×3 (6.0) y un círculo de radio 1 (≈3.142). totalArea recorrió referencias de Shape y llamó a area() sin comprobar de qué subtipo se trataba — cada subtipo era sustituible por su base (LSP).

Práctica

Práctica
Una clase llamada ReportGenerator tanto formatea los datos del informe como los escribe en disco, por lo que cualquier cambio en las reglas de formato o en el diseño del archivo te obliga a modificar y volver a probar la misma clase. ¿Qué principio SOLID viola esto más directamente?
Una clase llamada ReportGenerator tanto formatea los datos del informe como los escribe en disco, por lo que cualquier cambio en las reglas de formato o en el diseño del archivo te obliga a modificar y volver a probar la misma clase. ¿Qué principio SOLID viola esto más directamente?
Was this page helpful?