Los patrones de diseño no son recetas arbitrarias: cada uno existe para hacer cumplir uno o varios principios de diseño. Los principios son el porqué; los patrones, el cómo. Si estudias patrones sin principios, los aplicarás mecánicamente y donde no tocan; si dominas los principios, entenderás cada patrón a la primera y sabrás juzgar cuándo merece la pena. En esta lección estudiaremos los cinco principios SOLID con ejemplos de violación y corrección sobre PideYa, y los complementaremos con otros fundamentos imprescindibles: DRY, KISS, YAGNI y las dos máximas del Gang of Four. Es, probablemente, la lección más importante del módulo.

Contenido

  1. Qué es un principio de diseño y por qué precede a los patrones
  2. S — Principio de Responsabilidad Única (SRP)
  3. O — Principio Abierto/Cerrado (OCP)
  4. L — Principio de Sustitución de Liskov (LSP)
  5. I — Principio de Segregación de Interfaces (ISP)
  6. D — Principio de Inversión de Dependencias (DIP)
  7. Otros fundamentos: DRY, KISS y YAGNI
  8. Las dos máximas del GoF
  9. De los principios a los patrones

Qué es un principio de diseño y por qué precede a los patrones

Un principio de diseño es una directriz general sobre cómo organizar el código para que sea fácil de entender, cambiar y probar. A diferencia de un patrón, un principio no propone una estructura concreta de clases: propone un criterio de calidad. Los cinco más citados forman el acrónimo SOLID, popularizado por Robert C. Martin ("Uncle Bob"):

Letra Principio Idea en una frase
S Single Responsibility Una clase debe tener una única razón para cambiar
O Open/Closed Abierto a extensión, cerrado a modificación
L Liskov Substitution Las subclases deben poder usarse donde se espera la superclase
I Interface Segregation Mejor muchas interfaces pequeñas que una grande
D Dependency Inversion Depende de abstracciones, no de implementaciones concretas

Para cada principio seguiremos el mismo método: enunciado, violación en PideYa (código Java real y su problema) y corrección.

S — Principio de Responsabilidad Única (SRP)

Enunciado: una clase debe tener una única responsabilidad; dicho con más precisión, una única razón para cambiar. Si dos requisitos distintos (de dos "clientes" distintos: negocio, contabilidad, marketing...) obligan a tocar la misma clase, esa clase hace demasiado.

Violación en PideYa

public class Pedido {
    private List<LineaPedido> lineas;
    private Cliente cliente;

    public double calcularTotal() {
        return lineas.stream()
                     .mapToDouble(l -> l.getPrecio() * l.getCantidad())
                     .sum();
    }

    // ¿Responsabilidad de un pedido? Generar su propia factura en PDF...
    public byte[] generarFacturaPdf() {
        // ...lógica de maquetación, fuentes, logotipo...
        return new byte[0];
    }

    // ...¿y también guardarse a sí mismo en la base de datos?
    public void guardarEnBaseDeDatos() {
        // ...SQL, conexiones, transacciones...
    }
}

Esta clase tiene tres razones para cambiar: cambia si cambia la lógica de negocio (nuevos descuentos), si cambia el formato de la factura (nuevo logotipo, requisito legal), y si cambia la persistencia (migrar de MySQL a PostgreSQL). Tres equipos distintos tocando el mismo fichero: conflictos, riesgo y tests imposibles de aislar.

Corrección

public class Pedido {                 // Solo lógica de negocio del pedido
    private List<LineaPedido> lineas;
    public double calcularTotal() { /* ... */ return 0; }
}

public class GeneradorFacturas {      // Solo presentación de facturas
    public byte[] generarPdf(Pedido pedido) { /* ... */ return new byte[0]; }
}

public class RepositorioPedidos {     // Solo persistencia
    public void guardar(Pedido pedido) { /* ... */ }
}

Cada clase tiene ahora una responsabilidad y un motivo de cambio. Fíjate en que el SRP no dice "clases pequeñas": dice cohesionadas. Una clase de 300 líneas con una sola responsabilidad cumple el SRP; una de 30 líneas con dos, no.

O — Principio Abierto/Cerrado (OCP)

Enunciado: las entidades de software deben estar abiertas a la extensión (poder añadir comportamiento nuevo) pero cerradas a la modificación (sin editar el código existente que ya funciona). La herramienta para lograrlo es la abstracción: puntos de extensión mediante interfaces.

Violación en PideYa

PideYa aplica descuentos según el tipo de promoción:

public class CalculadoraDescuentos {
    public double calcular(Pedido pedido, String tipoPromocion) {
        if (tipoPromocion.equals("PRIMERA_COMPRA")) {
            return pedido.calcularTotal() * 0.10;
        } else if (tipoPromocion.equals("ENVIO_GRATIS")) {
            return pedido.getGastosEnvio();
        } else if (tipoPromocion.equals("CUPON_5_EUROS")) {
            return 5.0;
        }
        return 0;
    }
}

Cada promoción nueva de marketing obliga a modificar esta clase: otro else if, otra oportunidad de romper las promociones que ya funcionaban, otro despliegue con riesgo. La cadena de if/else if sobre un "tipo" es el síntoma clásico de violación del OCP.

Corrección

public interface Promocion {
    double calcularDescuento(Pedido pedido);
}

public class PromocionPrimeraCompra implements Promocion {
    public double calcularDescuento(Pedido pedido) {
        return pedido.calcularTotal() * 0.10;
    }
}

public class PromocionEnvioGratis implements Promocion {
    public double calcularDescuento(Pedido pedido) {
        return pedido.getGastosEnvio();
    }
}

public class CalculadoraDescuentos {
    public double calcular(Pedido pedido, Promocion promocion) {
        return promocion.calcularDescuento(pedido);  // Cerrada: no cambia nunca
    }
}

Añadir la promoción "2x1 en pizzas" es ahora añadir una clase nueva, sin tocar ni la calculadora ni las promociones existentes. El código probado permanece intacto. (Si esto te huele a un patrón con nombre propio, hueles bien: varios patrones del módulo 4 son exactamente esta jugada; no adelantemos.)

L — Principio de Sustitución de Liskov (LSP)

Enunciado (Barbara Liskov, 1987): si S es subtipo de T, los objetos de S deben poder usarse en cualquier lugar donde se espere un T sin que el programa deje de ser correcto. En la práctica: una subclase no puede endurecer las condiciones de entrada, debilitar las garantías de salida, ni lanzar sorpresas que la superclase no anunciaba.

Violación en PideYa

PideYa modela los métodos de pago, y a alguien le pareció natural que el pago contra reembolso "sea un" método de pago:

public class MetodoPago {
    /** Cobra el importe y devuelve un identificador de transacción. */
    public String cobrar(double importe) {
        // ...cobro online...
        return "TX-123";
    }
}

public class PagoContraReembolso extends MetodoPago {
    @Override
    public String cobrar(double importe) {
        // No se puede cobrar online: se paga al repartidor en la puerta
        throw new UnsupportedOperationException("No aplicable");
    }
}

El código cliente confía en el contrato de MetodoPago:

public void confirmarPedido(Pedido pedido, MetodoPago pago) {
    String tx = pago.cobrar(pedido.calcularTotal()); // ¡Explota con contra reembolso!
    pedido.marcarComoPagado(tx);
}

PagoContraReembolso es un MetodoPago según el compilador, pero no según el contrato: sustituirlo rompe el programa. Herencia sintácticamente válida, semánticamente fraudulenta.

Corrección

La solución es rediseñar la abstracción para que el contrato sea cumplible por todos los subtipos:

public interface MetodoPago {
    /** Devuelve el resultado del intento: cobrado ahora o pendiente a la entrega. */
    ResultadoPago procesar(double importe);
}

public class PagoTarjeta implements MetodoPago {
    public ResultadoPago procesar(double importe) {
        return ResultadoPago.cobrado("TX-123");
    }
}

public class PagoContraReembolso implements MetodoPago {
    public ResultadoPago procesar(double importe) {
        return ResultadoPago.pendienteEnEntrega(importe);
    }
}

Ahora todos los implementadores cumplen el mismo contrato ("procesar devuelve un resultado, nunca lanza por diseño") y el cliente puede tratar el resultado uniformemente. Regla práctica: si al heredar necesitas anular un método con una excepción o dejarlo vacío, la jerarquía está mal planteada.

I — Principio de Segregación de Interfaces (ISP)

Enunciado: ningún cliente debe verse obligado a depender de métodos que no usa. Mejor varias interfaces pequeñas y específicas que una interfaz "gorda" que lo mezcla todo.

Violación en PideYa

public interface Trabajador {
    void prepararPlato(Plato plato);
    void entregarPedido(Pedido pedido);
    void atenderLlamada(Llamada llamada);
}

public class Repartidor implements Trabajador {
    public void prepararPlato(Plato plato) {
        throw new UnsupportedOperationException(); // Un repartidor no cocina
    }
    public void entregarPedido(Pedido pedido) { /* ... */ }
    public void atenderLlamada(Llamada llamada) {
        throw new UnsupportedOperationException(); // ...ni atiende el teléfono
    }
}

Repartidor está obligado a "implementar" métodos que no le corresponden (y fíjate: la interfaz gorda le ha forzado a violar también el LSP). Además, si cambia la firma de prepararPlato, hay que recompilar y revisar a todos los que implementan Trabajador, cocinen o no.

Corrección

public interface Cocinero {
    void prepararPlato(Plato plato);
}

public interface Reparte {
    void entregarPedido(Pedido pedido);
}

public interface AtiendeTelefono {
    void atenderLlamada(Llamada llamada);
}

public class Repartidor implements Reparte {
    public void entregarPedido(Pedido pedido) { /* ... */ }
}

// Un empleado polivalente de un restaurante pequeño puede implementar varias:
public class EmpleadoPolivalente implements Cocinero, AtiendeTelefono {
    public void prepararPlato(Plato plato) { /* ... */ }
    public void atenderLlamada(Llamada llamada) { /* ... */ }
}

Cada cliente depende solo de lo que necesita, y los roles se componen libremente.

D — Principio de Inversión de Dependencias (DIP)

Enunciado: (1) los módulos de alto nivel (lógica de negocio) no deben depender de módulos de bajo nivel (detalles técnicos): ambos deben depender de abstracciones; (2) las abstracciones no deben depender de los detalles, sino los detalles de las abstracciones.

Violación en PideYa

public class ServicioPedidos {
    private ServicioSmsTwilio sms = new ServicioSmsTwilio(); // Detalle concreto

    public void confirmar(Pedido pedido) {
        // ...lógica de negocio...
        sms.enviarSms(pedido.getCliente().getTelefono(), "¡Pedido confirmado!");
    }
}

La lógica de negocio (alto nivel) depende de un proveedor concreto de SMS (bajo nivel), y además lo crea con new. Consecuencias: imposible cambiar de proveedor sin tocar el negocio, e imposible testear ServicioPedidos sin enviar SMS reales.

Corrección

public interface Notificador {                       // Abstracción, definida
    void notificar(Cliente cliente, String mensaje); // por el ALTO nivel
}

public class NotificadorSmsTwilio implements Notificador {  // Detalle
    public void notificar(Cliente cliente, String mensaje) {
        // ...API de Twilio...
    }
}

public class ServicioPedidos {
    private final Notificador notificador;

    public ServicioPedidos(Notificador notificador) {  // Inyección por constructor
        this.notificador = notificador;
    }

    public void confirmar(Pedido pedido) {
        // ...lógica de negocio...
        notificador.notificar(pedido.getCliente(), "¡Pedido confirmado!");
    }
}

La "inversión" del nombre está en la dirección de la dependencia: antes, negocio → Twilio; ahora, negocio → Notificador ← Twilio. El detalle depende de la abstracción del negocio, y no al revés. En los tests basta pasar un Notificador falso. (La inyección de dependencias que hacen frameworks como Spring es la automatización industrial de este principio.)

flowchart LR
    subgraph Antes
        A[ServicioPedidos] --> B[ServicioSmsTwilio]
    end
    subgraph Despues["Después"]
        C[ServicioPedidos] --> I[«interface» Notificador]
        D[NotificadorSmsTwilio] -.->|implementa| I
    end

Otros fundamentos: DRY, KISS y YAGNI

Tres principios más, menos formales pero igual de citados:

  • DRY (Don't Repeat Yourself): cada pieza de conocimiento debe tener una representación única en el sistema. Si la regla "el envío es gratis a partir de 20 €" está copiada en el carrito, en el checkout y en el email de confirmación de PideYa, el día que cambie a 25 € alguien olvidará uno de los tres sitios. Ojo: DRY habla de conocimiento, no de líneas parecidas; dos fragmentos accidentalmente iguales que evolucionarán por separado no deben unificarse.
  • KISS (Keep It Simple, Stupid): entre dos diseños que resuelven el problema, gana el más simple. La complejidad solo se justifica cuando compra algo (flexibilidad que se va a usar, rendimiento necesario). Este principio es el gran contrapeso de los patrones: cada patrón añade indirección, y KISS te obliga a preguntarte si la necesitas.
  • YAGNI (You Aren't Gonna Need It): no construyas hoy la flexibilidad que "quizá" necesites mañana. Si PideYa solo cobra con tarjeta y no hay planes de más, crear la jerarquía completa de MetodoPago "por si acaso" es coste sin beneficio. YAGNI no prohíbe diseñar bien: prohíbe especular. La señal para generalizar es un requisito real, no una corazonada.
Principio Protege contra... Tensión con los patrones
DRY Duplicación de conocimiento Los patrones ayudan a centralizar variaciones
KISS Complejidad innecesaria Todo patrón debe justificar su indirección
YAGNI Flexibilidad especulativa No apliques un patrón para un futuro imaginario

Las dos máximas del GoF

El libro del Gang of Four condensa su filosofía en dos máximas que reaparecerán en todos los módulos:

«Programa contra una interfaz, no contra una implementación»

Declara tus variables, parámetros y retornos con el tipo abstracto (interfaz o clase abstracta), no con el concreto. Ya lo has visto en acción en el OCP y el DIP:

// Mal: acoplado a la implementación
ArrayList<Plato> carta = new ArrayList<>();
NotificadorSmsTwilio notificador = new NotificadorSmsTwilio();

// Bien: acoplado solo al contrato
List<Plato> carta = new ArrayList<>();
Notificador notificador = obtenerNotificador();

El único punto que conoce la clase concreta es el de creación (new). Reducir y centralizar esos puntos es, exactamente, la misión de los patrones creacionales del módulo 2.

«Favorece la composición de objetos sobre la herencia de clases»

La herencia es tentadora para reutilizar código, pero acopla al hijo con las tripas del padre, se fija en tiempo de compilación y no se puede combinar (en Java solo se hereda de una clase). La composición —tener una referencia a otro objeto y delegar en él— es flexible, combinable y cambiable en tiempo de ejecución.

Ejemplo en PideYa: para modelar repartidores en moto o bicicleta, heredar RepartidorEnMoto y RepartidorEnBici de Repartidor explota en cuanto aparece otra dimensión (turno de día/noche → ¿RepartidorEnMotoDeNoche?). Con composición:

public class Repartidor {
    private Vehiculo vehiculo;   // Composición: "tiene un" vehículo

    public void asignarVehiculo(Vehiculo vehiculo) {  // Cambiable en caliente
        this.vehiculo = vehiculo;
    }

    public int tiempoEstimado(double km) {
        return vehiculo.calcularMinutos(km);          // Delegación
    }
}

Un repartidor puede cambiar de moto a bici a mitad de turno sin cambiar de clase. La herencia queda reservada para verdaderas relaciones "es-un" con contrato respetado (LSP). La mayoría de patrones estructurales y de comportamiento que verás son, en el fondo, formas ingeniosas de usar composición donde un principiante usaría herencia.

De los principios a los patrones

Cerremos con la idea central de la lección: los patrones son aplicaciones concretas, empaquetadas y con nombre, de estos principios. Cuando en los próximos módulos estudies un patrón, pregúntate siempre qué principios está sirviendo; como anticipo general (sin entrar en ningún patrón todavía):

  • Los patrones creacionales (módulo 2) existen sobre todo para servir al DIP y a "programa contra interfaces": aíslan los new para que el resto del código dependa solo de abstracciones.
  • Los patrones estructurales (módulo 3) son ejercicios de composición sobre herencia: envolver, adaptar y componer objetos.
  • Los patrones de comportamiento (módulo 4) explotan el OCP y el SRP: extraen comportamientos variables a jerarquías propias para extender sin modificar.

Y a la inversa: cuando detectes una violación de un principio (una cadena de if/else if por tipo, un new en medio del negocio, una subclase que lanza UnsupportedOperationException), tendrás la señal de que probablemente exista un patrón pensado para esa situación.

Errores Comunes y Consejos

  • Aplicar SRP como "clases diminutas". El criterio es razones para cambiar, no líneas de código. Fragmentar en exceso crea otro problema: lógica pulverizada en veinte clases anémicas.
  • Perseguir el OCP de forma preventiva. No pongas una interfaz delante de todo "por si acaso": eso viola YAGNI. Cierra contra modificación los ejes donde el cambio ya ha ocurrido o está anunciado (las promociones de PideYa cambian cada mes: ahí sí).
  • Verificar el LSP solo con el compilador. Que compile no significa que sustituya. Pregúntate: ¿puede todo código que usa la superclase recibir esta subclase sin sorpresas? Las excepciones inesperadas y los métodos vacíos son la señal de alarma.
  • Confundir DIP con "usar interfaces en todas partes". La inversión está en quién define la abstracción (el alto nivel) y en hacia dónde apuntan las dependencias, no en el número de interfaces.
  • Tratar DRY, KISS y YAGNI como absolutos. Son fuerzas a equilibrar: DRY empuja a abstraer, KISS y YAGNI a no abstraer de más. El buen diseño vive en la tensión, no en el extremo.
  • Consejo: en tu próxima revisión de código, busca solo dos síntomas: cadenas de if/else if sobre un "tipo" y new de clases concretas dentro de lógica de negocio. Son las dos violaciones más frecuentes y las que más patrones motivan.

Ejercicios

Ejercicio 1: diagnosticar violaciones

Esta clase de PideYa viola varios principios SOLID. Identifica al menos tres, indicando cuáles y por qué:

public class GestorRestaurante {
    private ConexionMySql conexion = new ConexionMySql();

    public void darDeAltaPlato(String nombre, double precio, String tipo) {
        if (tipo.equals("PIZZA")) {
            // validaciones específicas de pizzas
        } else if (tipo.equals("SUSHI")) {
            // validaciones específicas de sushi
        }
        conexion.ejecutar("INSERT INTO platos ...");
        enviarEmailAlDuenyo(nombre);
    }

    private void enviarEmailAlDuenyo(String nombrePlato) {
        // SMTP, plantillas HTML, reintentos...
    }
}

Ejercicio 2: refactorizar hacia OCP + DIP

PideYa calcula los gastos de envío según la zona con este código. Refactorízalo para que añadir una zona nueva no requiera modificar la clase, y para que la clase de negocio no cree sus dependencias:

public class CalculadoraEnvio {
    public double calcular(Pedido pedido) {
        String zona = pedido.getDireccion().getZona();
        if (zona.equals("CENTRO")) return 1.50;
        else if (zona.equals("PERIFERIA")) return 3.00;
        else return 5.00;
    }
}

Ejercicio 3: herencia contra composición

Un compañero propone modelar los platos con descuento como subclases: PlatoConDescuento10 extends Plato, PlatoConDescuento20 extends Plato. Argumenta por qué es mala idea (cita al menos dos principios o máximas de esta lección) y esboza en pseudocódigo una alternativa basada en composición.

Soluciones

Solución 1:

  • SRP: la clase mezcla validación de negocio, persistencia (SQL) y envío de emails: tres razones para cambiar.
  • OCP: la cadena if/else if por tipo de plato obliga a modificar el método con cada tipo nuevo.
  • DIP: el negocio depende de ConexionMySql concreta y la crea con new; debería depender de una abstracción de persistencia inyectada. (También puede argumentarse que el email incrustado viola de nuevo SRP/DIP: detalle técnico dentro del alto nivel.)

Solución 2 (una solución posible):

public interface TarifaZona {
    boolean aplicaA(Direccion direccion);
    double coste();
}

public class TarifaCentro implements TarifaZona {
    public boolean aplicaA(Direccion d) { return d.getZona().equals("CENTRO"); }
    public double coste() { return 1.50; }
}
// TarifaPeriferia, TarifaEstandar... análogas

public class CalculadoraEnvio {
    private final List<TarifaZona> tarifas;
    private final double costePorDefecto;

    public CalculadoraEnvio(List<TarifaZona> tarifas, double costePorDefecto) {
        this.tarifas = tarifas;               // Inyectadas: DIP
        this.costePorDefecto = costePorDefecto;
    }

    public double calcular(Pedido pedido) {
        return tarifas.stream()
                .filter(t -> t.aplicaA(pedido.getDireccion()))
                .findFirst()
                .map(TarifaZona::coste)
                .orElse(costePorDefecto);     // OCP: zonas nuevas = clases nuevas
    }
}

Añadir la zona "URBANIZACIONES" es crear una clase e incluirla en la lista inyectada: la calculadora no se toca.

Solución 3: argumentos: (1) explosión combinatoria y rigidez de la herencia: cada porcentaje nuevo es una subclase, y un plato no puede ganar o perder el descuento en tiempo de ejecución (un descuento es temporal por naturaleza), lo que choca con "favorece la composición sobre la herencia"; (2) OCP/DRY: la lógica "aplicar un porcentaje" queda duplicada en cada subclase y añadir el 15% exige una clase nueva idéntica a las otras; además la relación real no es "es-un" (un plato con descuento no es un tipo distinto de plato), señal de herencia mal usada (el espíritu del LSP). Alternativa en pseudocódigo:

clase Plato:
    precioBase
    descuento: Descuento        // composición, puede ser "sin descuento"
    precioFinal() = descuento.aplicarA(precioBase)

interfaz Descuento:
    aplicarA(precio)

clase DescuentoPorcentual implementa Descuento(porcentaje)
clase SinDescuento implementa Descuento   // devuelve el precio tal cual

El descuento se asigna, cambia o retira en tiempo de ejecución sin tocar la clase Plato.

Conclusión

Ya tienes el sistema de valores del diseño orientado a objetos: SOLID (una responsabilidad por clase, extender sin modificar, subtipos que sustituyen de verdad, interfaces a medida del cliente y dependencias que apuntan a abstracciones), templado por DRY, KISS y YAGNI, y coronado por las dos máximas del GoF: programar contra interfaces y favorecer la composición. Y, sobre todo, la clave de bóveda del curso: cada patrón que estudiarás es uno o varios de estos principios convertidos en estructura concreta; los principios te dirán además cuándo un patrón sobra.

Para estudiar esas estructuras necesitamos poder dibujarlas y leerlas: los patrones se comunican con diagramas de clases y de secuencia. En la próxima lección aprenderás justo el UML imprescindible para este curso: UML Esencial para Entender Patrones.

Curso de Patrones de Diseño de Software

Módulo 1: Introducción a los Patrones de Diseño

Módulo 2: Patrones Creacionales

Módulo 3: Patrones Estructurales

Módulo 4: Patrones de Comportamiento

Módulo 5: Aplicación de Patrones de Diseño

Módulo 6: Patrones de Diseño Avanzados

Módulo 7: Recursos Adicionales y Conclusión

© Copyright 2026. Todos los derechos reservados