Patrones de Diseño en Java

📅 Actualizado en febrero 2026 ✍️ Ángel López ⏱️ 25 min de lectura ✓ Nivel intermedio-avanzado

🏗️ ¿Qué son los patrones de diseño?

Los patrones de diseño (design patterns) son soluciones probadas y reutilizables a problemas recurrentes que aparecen durante el diseño de software orientado a objetos. No son fragmentos de código que se copian y pegan, sino plantillas conceptuales que describen cómo estructurar clases y objetos para resolver un tipo específico de problema de forma elegante y mantenible.

Podemos pensar en ellos como el equivalente informático de los planos arquitectónicos: un arquitecto no diseña cada edificio desde cero, sino que aplica soluciones conocidas (arcos, contrafuertes, bóvedas) adaptándolas al contexto específico. De la misma manera, un desarrollador Java puede aplicar un patrón Observer para desacoplar componentes o un patrón Strategy para hacer intercambiables los algoritmos de un sistema.

💡 Definición formal: Un patrón de diseño describe un problema que ocurre repetidamente en nuestro entorno, la esencia de su solución, de manera que puedas usar esa solución un millón de veces más sin hacerlo nunca dos veces de la misma forma. — Christopher Alexander, A Pattern Language (1977).

Cada patrón de diseño tiene cuatro elementos esenciales que lo definen:

Elemento Descripción Ejemplo
Nombre Identifica el patrón con una o dos palabras Singleton, Observer, Factory
Problema Describe cuándo aplicar el patrón «Necesito que solo exista una instancia de una clase»
Solución Describe los elementos del diseño, sus relaciones y responsabilidades Constructor privado + método estático de acceso
Consecuencias Resultados, ventajas y compromisos de aplicar el patrón Control de acceso global, pero dificulta testing

📜 Origen histórico: del GoF a la actualidad

El concepto de patrón aplicado al diseño de software tiene una historia fascinante que se remonta a la arquitectura. En 1977, el arquitecto Christopher Alexander publicó A Pattern Language, donde catalogó 253 patrones arquitectónicos para el diseño de edificios y ciudades. Su idea central era que los patrones son soluciones recurrentes a problemas dentro de un contexto determinado.

La comunidad informática adoptó esta idea en la década de 1980. En 1987, Kent Beck y Ward Cunningham presentaron por primera vez la aplicación de patrones al diseño de software en la conferencia OOPSLA. Sin embargo, el verdadero punto de inflexión llegó en 1994, cuando cuatro autores —Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides— publicaron el libro que revolucionaría la ingeniería de software:

El libro fundacional: Design Patterns: Elements of Reusable Object-Oriented Software (1994). Sus autores son conocidos como el Gang of Four (GoF) — la «Banda de los Cuatro». Este libro catalogó 23 patrones de diseño que siguen siendo la referencia fundamental tres décadas después.

El impacto del GoF fue enorme: creó un vocabulario común entre desarrolladores de todo el mundo. Antes del libro, un programador podía describir su solución en un párrafo; después del GoF, bastaba con decir «apliqué un Observer» para que cualquier colega comprendiera instantáneamente la estructura del diseño. Este vocabulario compartido aceleró la comunicación en equipos y la revisión de código de forma extraordinaria.

Desde 1994, la disciplina ha seguido evolucionando. Martin Fowler publicó Patterns of Enterprise Application Architecture (2002), que extendió los patrones al mundo empresarial con patrones como Repository, Unit of Work y Data Mapper. Más recientemente, los patrones se han adaptado a nuevos paradigmas como la programación reactiva, los microservicios y la arquitectura en la nube.

📊 Clasificación de los 23 patrones GoF

Los 23 patrones del catálogo GoF se organizan en tres categorías según el tipo de problema que resuelven. Esta clasificación es fundamental para orientarse en el catálogo y seleccionar el patrón adecuado para cada situación.

Categoría Propósito Patrones (23 total)
🔨 Creacionales (5) Abstraen el proceso de creación de objetos, haciendo el sistema independiente de cómo se crean, componen y representan Singleton, Factory Method, Abstract Factory, Builder, Prototype
🧱 Estructurales (7) Describen cómo componer clases y objetos para formar estructuras más grandes y complejas Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
⚙️ Comportamiento (11) Se ocupan de los algoritmos y la asignación de responsabilidades entre objetos Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor

Además de esta clasificación por propósito, los patrones pueden clasificarse por ámbito: los patrones de clase utilizan herencia para componer o variar las clases en tiempo de compilación, mientras que los patrones de objeto utilizan composición y pueden cambiar en tiempo de ejecución. En Java moderno, los patrones de objeto son más frecuentes porque favorecen la composición sobre la herencia, un principio fundamental del buen diseño.

🔨 Patrones creacionales

Los patrones creacionales resuelven un problema fundamental en el diseño orientado a objetos: ¿cómo crear objetos de forma flexible sin acoplar el código a clases concretas? Cuando usamos new MiClase() directamente, estamos creando una dependencia rígida. Los patrones creacionales proporcionan mecanismos para desacoplar la creación de objetos de su uso.

🔹 Singleton

Garantiza que una clase tenga exactamente una instancia y proporciona un punto de acceso global a ella. Útil para gestores de configuración, pools de conexiones o registros de log.

🔹 Factory Method

Define una interfaz para crear un objeto, pero deja que las subclases decidan qué clase instanciar. Permite que una clase delegue la creación a sus subclases. Es uno de los patrones más utilizados en frameworks como Spring.

🔹 Abstract Factory

Proporciona una interfaz para crear familias de objetos relacionados sin especificar sus clases concretas. Es como una «fábrica de fábricas». Ejemplo típico: crear componentes de interfaz gráfica que funcionen tanto en Windows como en macOS.

🔹 Builder

Separa la construcción de un objeto complejo de su representación, permitiendo que el mismo proceso de construcción cree diferentes representaciones. Ideal para objetos con muchos parámetros opcionales, como lo demuestra el patrón Builder de la clase StringBuilder o HttpRequest.newBuilder() en Java moderno.

🔹 Prototype

Especifica los tipos de objetos a crear mediante una instancia prototípica y crea nuevos objetos copiando este prototipo. En Java, se implementa con la interfaz Cloneable y el método clone().

🧱 Patrones estructurales

Los patrones estructurales se ocupan de cómo se componen clases y objetos para formar estructuras mayores. Facilitan el diseño identificando formas sencillas de establecer relaciones entre entidades. Son especialmente importantes en Java, donde las interfaces y las clases abstractas permiten composiciones muy flexibles.

🔹 Adapter (Adaptador)

Convierte la interfaz de una clase en otra que el cliente espera. Permite que clases incompatibles trabajen juntas. Ejemplo en Java: Arrays.asList() adapta un array a la interfaz List.

🔹 Bridge (Puente)

Desacopla una abstracción de su implementación para que ambas puedan variar independientemente. Mientras que Adapter hace que interfaces incompatibles funcionen juntas después del diseño, Bridge se aplica durante el diseño para separar abstracción e implementación desde el principio.

🔹 Composite (Compuesto)

Compone objetos en estructuras de árbol para representar jerarquías parte-todo. Permite que los clientes traten objetos individuales y composiciones de forma uniforme. Ejemplo clásico: un sistema de archivos donde tanto archivos como carpetas implementan la misma interfaz.

🔹 Decorator (Decorador)

Añade responsabilidades adicionales a un objeto de forma dinámica, proporcionando una alternativa flexible a la herencia. El sistema de I/O de Java es un ejemplo canónico: new BufferedReader(new InputStreamReader(new FileInputStream("archivo.txt"))).

🔹 Facade (Fachada)

Proporciona una interfaz simplificada a un subsistema complejo. No encapsula el subsistema, sino que ofrece una «puerta de entrada» cómoda. Ejemplo: una clase PedidoService que internamente coordina inventario, pagos y envíos.

🔹 Flyweight (Peso ligero)

Usa compartición para soportar eficientemente grandes cantidades de objetos de grano fino. En Java, String.intern() y el Integer Cache (-128 a 127) son implementaciones del patrón Flyweight.

🔹 Proxy

Proporciona un sustituto o representante de otro objeto para controlar el acceso a este. Existen varias variantes: proxy remoto (RMI), proxy virtual (carga diferida), proxy de protección (control de acceso) y proxy de caché. Java ofrece soporte nativo con java.lang.reflect.Proxy.

⚙️ Patrones de comportamiento

Los patrones de comportamiento se centran en los algoritmos y la distribución de responsabilidades entre objetos. No solo describen patrones de objetos y clases, sino también los patrones de comunicación entre ellos. Son particularmente útiles para diseñar sistemas donde múltiples objetos colaboran de forma compleja.

🔹 Observer (Observador)

Define una dependencia uno-a-muchos entre objetos, de forma que cuando un objeto cambia de estado, todos sus dependientes son notificados automáticamente. Base de toda la programación dirigida por eventos en Java (listeners de Swing, PropertyChangeListener, sistemas de eventos).

🔹 Strategy (Estrategia)

Define una familia de algoritmos, encapsula cada uno y los hace intercambiables. Permite que el algoritmo varíe independientemente del cliente que lo usa. En Java, la interfaz Comparator es un ejemplo perfecto de Strategy.

🔹 Iterator (Iterador)

Proporciona una forma de acceder secuencialmente a los elementos de una colección sin exponer su representación interna. En Java, es tan fundamental que tiene soporte del lenguaje mediante la interfaz Iterable y el bucle for-each.

🔹 Template Method (Método Plantilla)

Define el esqueleto de un algoritmo en una operación, delegando algunos pasos a las subclases. Permite que las subclases redefinan ciertos pasos sin cambiar la estructura del algoritmo. Ejemplo: HttpServlet.doGet() y doPost().

🔹 Command (Comando)

Encapsula una solicitud como un objeto, permitiendo parametrizar clientes con diferentes solicitudes, encolar solicitudes o registrarlas para operaciones de deshacer (undo). Fundamental en interfaces gráficas y sistemas de colas de mensajes.

🔹 State (Estado)

Permite que un objeto altere su comportamiento cuando cambia su estado interno. El objeto parecerá haber cambiado de clase. Ideal para máquinas de estados: un pedido que pasa por estados Pendiente → Pagado → Enviado → Entregado.

🔹 Otros patrones de comportamiento

El catálogo GoF incluye además Chain of Responsibility (cadena de manejadores), Mediator (coordinador central entre objetos), Memento (captura y restauración de estado), Interpreter (gramáticas simples) y Visitor (operaciones sobre estructuras de objetos sin modificar las clases). Cada uno tiene aplicaciones específicas en diseño de software empresarial.

🔐 Singleton: implementación profesional en Java

El patrón Singleton es probablemente el más conocido y también el más debatido. Su objetivo es sencillo: garantizar que una clase tenga exactamente una instancia y proporcionar un punto de acceso global. Veamos las distintas formas de implementarlo en Java, desde la más básica hasta la recomendada profesionalmente.

🔹 Implementación clásica (lazy initialization)

SingletonBasico.java
public class SingletonBasico {

    // Instancia única, inicialmente null
    private static SingletonBasico instancia;

    // Constructor privado: nadie externo puede crear instancias
    private SingletonBasico() {
        System.out.println("Singleton creado");
    }

    // Punto de acceso global (NO thread-safe)
    public static SingletonBasico getInstancia() {
        if (instancia == null) {
            instancia = new SingletonBasico();
        }
        return instancia;
    }

    public void operacion() {
        System.out.println("Operación del Singleton ejecutada");
    }
}

⚠️ Problema: Esta implementación NO es segura en entornos multihilo (thread-safe). Si dos hilos llaman a getInstancia() simultáneamente cuando instancia aún es null, podrían crearse dos instancias.

🔹 Implementación recomendada: Bill Pugh (Holder idiom)

La implementación más elegante y eficiente en Java aprovecha la carga diferida de clases internas estáticas. Es thread-safe sin necesidad de sincronización explícita:

SingletonBillPugh.java
public class SingletonBillPugh {

    // Constructor privado
    private SingletonBillPugh() {
        System.out.println("Singleton creado de forma segura");
    }

    // Clase interna estática: se carga SOLO cuando se referencia
    private static class Holder {
        private static final SingletonBillPugh INSTANCIA = new SingletonBillPugh();
    }

    // Thread-safe por garantía de la JVM (carga de clases es atómica)
    public static SingletonBillPugh getInstancia() {
        return Holder.INSTANCIA;
    }

    public void configurar(String parametro) {
        System.out.println("Configurando con: " + parametro);
    }
}

// Uso:
// SingletonBillPugh config = SingletonBillPugh.getInstancia();
// config.configurar("produccion");

¿Por qué es la mejor opción? La JVM garantiza que la clase interna Holder solo se carga cuando se invoca getInstancia(). La inicialización de campos estáticos es atómica por especificación de la JVM (JLS §12.4.2), así que es inherentemente thread-safe sin el coste de synchronized.

🏭 Factory Method: delegando la creación

El patrón Factory Method define una interfaz para crear un objeto, pero permite que las subclases decidan qué clase concreta instanciar. Es el patrón más utilizado en frameworks Java como Spring, donde la creación de beans se delega completamente al contenedor.

FactoryMethod.java — Sistema de documentos
// Producto: interfaz común
public interface Documento {
    void abrir();
    void guardar();
    String getTipo();
}

// Productos concretos
public class DocumentoPDF implements Documento {
    @Override
    public void abrir() {
        System.out.println("Abriendo documento PDF con visor integrado");
    }

    @Override
    public void guardar() {
        System.out.println("Guardando PDF con compresión estándar");
    }

    @Override
    public String getTipo() { return "PDF"; }
}

public class DocumentoWord implements Documento {
    @Override
    public void abrir() {
        System.out.println("Abriendo documento Word en modo edición");
    }

    @Override
    public void guardar() {
        System.out.println("Guardando Word con formato OOXML");
    }

    @Override
    public String getTipo() { return "Word"; }
}

public class DocumentoCSV implements Documento {
    @Override
    public void abrir() {
        System.out.println("Abriendo CSV en vista tabular");
    }

    @Override
    public void guardar() {
        System.out.println("Guardando CSV con delimitador punto y coma");
    }

    @Override
    public String getTipo() { return "CSV"; }
}

// Creador abstracto: define el Factory Method
public abstract class EditorDocumentos {

    // Factory Method: las subclases deciden qué Documento crear
    protected abstract Documento crearDocumento();

    // Método que usa el Factory Method (Template Method implícito)
    public void nuevoDocumento() {
        Documento doc = crearDocumento();  // Delegación a subclases
        System.out.println("Creado documento de tipo: " + doc.getTipo());
        doc.abrir();
    }
}

// Creadores concretos
public class EditorPDF extends EditorDocumentos {
    @Override
    protected Documento crearDocumento() {
        return new DocumentoPDF();
    }
}

public class EditorWord extends EditorDocumentos {
    @Override
    protected Documento crearDocumento() {
        return new DocumentoWord();
    }
}

// Uso:
// EditorDocumentos editor = new EditorPDF();
// editor.nuevoDocumento();
// Salida:
//   Creado documento de tipo: PDF
//   Abriendo documento PDF con visor integrado

La ventaja clave es que el código cliente trabaja con la abstracción EditorDocumentos sin conocer la clase concreta. Para añadir un nuevo tipo de documento (por ejemplo, DocumentoJSON), basta con crear la clase concreta y su editor correspondiente, sin modificar el código existente. Esto cumple el Principio Abierto/Cerrado (Open/Closed Principle) de SOLID.

👁️ Observer: comunicación desacoplada

El patrón Observer establece una relación uno-a-muchos entre objetos: cuando el sujeto cambia de estado, todos los observadores registrados son notificados automáticamente. Es la base de toda la programación orientada a eventos y es omnipresente en interfaces gráficas, sistemas de mensajería y arquitecturas reactivas.

Observer.java — Sistema de monitorización de precios
import java.util.ArrayList;
import java.util.List;

// Interfaz del observador
public interface ObservadorPrecio {
    void actualizar(String producto, double precioAnterior, double precioNuevo);
}

// Sujeto observable
public class ProductoMonitorizado {
    private String nombre;
    private double precio;
    private List<ObservadorPrecio> observadores = new ArrayList<>();

    public ProductoMonitorizado(String nombre, double precioInicial) {
        this.nombre = nombre;
        this.precio = precioInicial;
    }

    public void agregarObservador(ObservadorPrecio obs) {
        observadores.add(obs);
    }

    public void eliminarObservador(ObservadorPrecio obs) {
        observadores.remove(obs);
    }

    // Al cambiar el precio, se notifica a todos los observadores
    public void setPrecio(double nuevoPrecio) {
        double anterior = this.precio;
        this.precio = nuevoPrecio;
        if (anterior != nuevoPrecio) {
            notificarObservadores(anterior, nuevoPrecio);
        }
    }

    private void notificarObservadores(double anterior, double nuevo) {
        for (ObservadorPrecio obs : observadores) {
            obs.actualizar(nombre, anterior, nuevo);
        }
    }

    public double getPrecio() { return precio; }
    public String getNombre() { return nombre; }
}

// Observadores concretos
public class AlertaEmail implements ObservadorPrecio {
    private String destinatario;

    public AlertaEmail(String destinatario) {
        this.destinatario = destinatario;
    }

    @Override
    public void actualizar(String producto, double anterior, double nuevo) {
        if (nuevo < anterior) {
            System.out.printf("[EMAIL → %s] ¡%s ha bajado de %.2f€ a %.2f€!%n",
                destinatario, producto, anterior, nuevo);
        }
    }
}

public class RegistroHistorial implements ObservadorPrecio {
    @Override
    public void actualizar(String producto, double anterior, double nuevo) {
        double variacion = ((nuevo - anterior) / anterior) * 100;
        System.out.printf("[HISTORIAL] %s: %.2f€ → %.2f€ (%.1f%%)%n",
            producto, anterior, nuevo, variacion);
    }
}

// Uso:
// ProductoMonitorizado laptop = new ProductoMonitorizado("MacBook Pro", 2499.00);
// laptop.agregarObservador(new AlertaEmail("ana@empresa.com"));
// laptop.agregarObservador(new RegistroHistorial());
// laptop.setPrecio(2299.00);
// Salida:
//   [EMAIL → ana@empresa.com] ¡MacBook Pro ha bajado de 2499.00€ a 2299.00€!
//   [HISTORIAL] MacBook Pro: 2499.00€ → 2299.00€ (-8.0%)

🎯 Strategy: algoritmos intercambiables

El patrón Strategy encapsula una familia de algoritmos, haciendo que sean intercambiables en tiempo de ejecución. El cliente elige la estrategia adecuada según el contexto, sin que el código que la utiliza tenga que cambiar. En Java, este patrón se simplifica enormemente con las expresiones lambda introducidas en Java 8.

Strategy.java — Sistema de cálculo de descuentos
// Interfaz Strategy
@FunctionalInterface
public interface EstrategiaDescuento {
    double calcularDescuento(double precioOriginal);
}

// Estrategias concretas
public class DescuentoPorcentaje implements EstrategiaDescuento {
    private double porcentaje;

    public DescuentoPorcentaje(double porcentaje) {
        this.porcentaje = porcentaje;
    }

    @Override
    public double calcularDescuento(double precioOriginal) {
        return precioOriginal * (porcentaje / 100.0);
    }
}

public class DescuentoCantidadFija implements EstrategiaDescuento {
    private double cantidad;

    public DescuentoCantidadFija(double cantidad) {
        this.cantidad = cantidad;
    }

    @Override
    public double calcularDescuento(double precioOriginal) {
        return Math.min(cantidad, precioOriginal); // No superar el precio
    }
}

public class DescuentoBlackFriday implements EstrategiaDescuento {
    @Override
    public double calcularDescuento(double precioOriginal) {
        // 30% de descuento + 10€ adicionales si supera 100€
        double descuento = precioOriginal * 0.30;
        if (precioOriginal > 100.0) {
            descuento += 10.0;
        }
        return Math.min(descuento, precioOriginal);
    }
}

// Contexto que usa la estrategia
public class CarritoCompra {
    private double total;
    private EstrategiaDescuento estrategia;

    public CarritoCompra(double total) {
        this.total = total;
    }

    // Cambiar estrategia en tiempo de ejecución
    public void setEstrategiaDescuento(EstrategiaDescuento estrategia) {
        this.estrategia = estrategia;
    }

    public double calcularPrecioFinal() {
        if (estrategia == null) {
            return total;
        }
        double descuento = estrategia.calcularDescuento(total);
        return total - descuento;
    }

    public void mostrarResumen() {
        double descuento = (estrategia != null)
            ? estrategia.calcularDescuento(total) : 0;
        System.out.printf("Total: %.2f€ | Descuento: %.2f€ | Final: %.2f€%n",
            total, descuento, total - descuento);
    }
}

// Uso:
// CarritoCompra carrito = new CarritoCompra(250.00);
//
// carrito.setEstrategiaDescuento(new DescuentoPorcentaje(15));
// carrito.mostrarResumen();
// → Total: 250.00€ | Descuento: 37.50€ | Final: 212.50€
//
// carrito.setEstrategiaDescuento(new DescuentoBlackFriday());
// carrito.mostrarResumen();
// → Total: 250.00€ | Descuento: 85.00€ | Final: 165.00€
//
// // Con lambda (Java 8+):
// carrito.setEstrategiaDescuento(precio -> precio * 0.05);
// carrito.mostrarResumen();
// → Total: 250.00€ | Descuento: 12.50€ | Final: 237.50€

💡 Strategy con lambdas: Desde Java 8, si la interfaz Strategy tiene un solo método abstracto (es @FunctionalInterface), puedes pasar expresiones lambda directamente en lugar de crear clases concretas. Esto reduce drásticamente el código boilerplate para estrategias simples.

🚀 Ejemplo integrador: sistema de notificaciones

Este ejemplo combina tres patrones en un sistema realista de notificaciones para una plataforma de e-commerce: Factory Method para crear los canales de notificación, Strategy para las políticas de prioridad, y Observer para la suscripción de clientes a eventos de pedido.

SistemaNotificaciones.java — Tres patrones combinados
import java.util.ArrayList;
import java.util.List;

// === STRATEGY: Política de prioridad ===
@FunctionalInterface
interface PoliticaPrioridad {
    boolean debeNotificar(String tipoEvento, String nivelCliente);
}

class PrioridadTodo implements PoliticaPrioridad {
    @Override
    public boolean debeNotificar(String tipoEvento, String nivelCliente) {
        return true; // Notificar siempre
    }
}

class PrioridadSoloVIP implements PoliticaPrioridad {
    @Override
    public boolean debeNotificar(String tipoEvento, String nivelCliente) {
        return "VIP".equals(nivelCliente);
    }
}

// === FACTORY METHOD: Canales de notificación ===
interface CanalNotificacion {
    void enviar(String destinatario, String mensaje);
    String getNombreCanal();
}

class CanalEmail implements CanalNotificacion {
    @Override
    public void enviar(String destinatario, String mensaje) {
        System.out.printf("  📧 EMAIL → %s: %s%n", destinatario, mensaje);
    }
    @Override
    public String getNombreCanal() { return "Email"; }
}

class CanalSMS implements CanalNotificacion {
    @Override
    public void enviar(String destinatario, String mensaje) {
        System.out.printf("  📱 SMS → %s: %s%n", destinatario, mensaje);
    }
    @Override
    public String getNombreCanal() { return "SMS"; }
}

class CanalPush implements CanalNotificacion {
    @Override
    public void enviar(String destinatario, String mensaje) {
        System.out.printf("  🔔 PUSH → %s: %s%n", destinatario, mensaje);
    }
    @Override
    public String getNombreCanal() { return "Push"; }
}

// Factory Method
abstract class FabricaCanal {
    abstract CanalNotificacion crearCanal();
}

class FabricaEmail extends FabricaCanal {
    @Override
    CanalNotificacion crearCanal() { return new CanalEmail(); }
}

class FabricaSMS extends FabricaCanal {
    @Override
    CanalNotificacion crearCanal() { return new CanalSMS(); }
}

// === OBSERVER: Suscriptores de eventos ===
interface SuscriptorPedido {
    void notificar(String tipoEvento, String detalle);
    String getIdentificador();
    String getNivelCliente();
}

class ClienteSuscriptor implements SuscriptorPedido {
    private String nombre;
    private String contacto;
    private String nivel;
    private CanalNotificacion canal;

    public ClienteSuscriptor(String nombre, String contacto,
                              String nivel, CanalNotificacion canal) {
        this.nombre = nombre;
        this.contacto = contacto;
        this.nivel = nivel;
        this.canal = canal;
    }

    @Override
    public void notificar(String tipoEvento, String detalle) {
        canal.enviar(contacto, "[" + tipoEvento + "] " + detalle);
    }

    @Override
    public String getIdentificador() { return nombre; }

    @Override
    public String getNivelCliente() { return nivel; }
}

// Servicio central (Sujeto Observable + usa Strategy)
class ServicioPedidos {
    private List<SuscriptorPedido> suscriptores = new ArrayList<>();
    private PoliticaPrioridad politica = new PrioridadTodo(); // Strategy

    public void setPoliticaPrioridad(PoliticaPrioridad politica) {
        this.politica = politica;
    }

    public void suscribir(SuscriptorPedido s) {
        suscriptores.add(s);
        System.out.println("✓ Suscrito: " + s.getIdentificador());
    }

    public void procesarPedido(String pedidoId, String detalle) {
        System.out.println("\n🛒 Procesando pedido " + pedidoId + "...");
        emitirEvento("PEDIDO_CONFIRMADO",
            "Pedido " + pedidoId + " - " + detalle);
    }

    private void emitirEvento(String tipo, String detalle) {
        System.out.println("  Notificando (política: "
            + politica.getClass().getSimpleName() + "):");
        for (SuscriptorPedido s : suscriptores) {
            if (politica.debeNotificar(tipo, s.getNivelCliente())) {
                s.notificar(tipo, detalle);
            } else {
                System.out.printf("  ⏭️ Omitido: %s (nivel: %s)%n",
                    s.getIdentificador(), s.getNivelCliente());
            }
        }
    }
}

// === PROGRAMA PRINCIPAL ===
// ServicioPedidos servicio = new ServicioPedidos();
//
// // Factory Method crea los canales
// CanalNotificacion email = new FabricaEmail().crearCanal();
// CanalNotificacion sms   = new FabricaSMS().crearCanal();
//
// // Observer: suscribir clientes
// servicio.suscribir(new ClienteSuscriptor("Ana", "ana@mail.com", "VIP", email));
// servicio.suscribir(new ClienteSuscriptor("Luis", "+34600123456", "Standard", sms));
//
// // Strategy: notificar a todos
// servicio.setPoliticaPrioridad(new PrioridadTodo());
// servicio.procesarPedido("P-1001", "MacBook Pro 16\"");
//
// // Cambiar Strategy: solo VIP
// servicio.setPoliticaPrioridad(new PrioridadSoloVIP());
// servicio.procesarPedido("P-1002", "iPhone 16 Pro");

// Salida:
// ✓ Suscrito: Ana
// ✓ Suscrito: Luis
//
// 🛒 Procesando pedido P-1001...
//   Notificando (política: PrioridadTodo):
//   📧 EMAIL → ana@mail.com: [PEDIDO_CONFIRMADO] Pedido P-1001 - MacBook Pro 16"
//   📱 SMS → +34600123456: [PEDIDO_CONFIRMADO] Pedido P-1001 - MacBook Pro 16"
//
// 🛒 Procesando pedido P-1002...
//   Notificando (política: PrioridadSoloVIP):
//   📧 EMAIL → ana@mail.com: [PEDIDO_CONFIRMADO] Pedido P-1002 - iPhone 16 Pro
//   ⏭️ Omitido: Luis (nivel: Standard)

🐛 Errores frecuentes al usar patrones

❌ Error 1: Sobreingeniería (Pattern-itis)

El error más común es intentar aplicar patrones en todas partes, incluso donde no son necesarios. Un sistema sencillo con tres clases no necesita un Abstract Factory, un Builder y un Mediator. La regla práctica es: aplica un patrón cuando el problema que resuelve ya existe en tu código, no «por si acaso» aparece en el futuro.

Sobreingeniería vs. Solución directa
// ❌ SOBREINGENIERÍA: Factory + Strategy + Builder para una suma
SumaFactory factory = new SumaFactoryImpl();
EstrategiaSuma estrategia = factory.crearEstrategia("basica");
ResultadoSuma resultado = new ResultadoSumaBuilder()
    .conOperandoA(3)
    .conOperandoB(5)
    .conEstrategia(estrategia)
    .build();

// ✅ SOLUCIÓN DIRECTA: para una suma, basta con sumar
int resultado = 3 + 5;

❌ Error 2: Singleton como variable global

Usar Singleton para compartir estado mutable a través de toda la aplicación lo convierte en una variable global encubierta. Esto genera acoplamiento oculto, dificulta las pruebas unitarias y hace que el código sea impredecible. En aplicaciones modernas, la inyección de dependencias (Spring, CDI) resuelve este problema de forma más limpia.

❌ Error 3: Observer sin desregistro

Olvidar eliminar los observadores cuando ya no son necesarios causa memory leaks. El sujeto mantiene referencias a observadores que deberían haberse recolectado por el garbage collector. Solución: usar WeakReference o garantizar un mecanismo de desregistro explícito en el ciclo de vida del observador.

❌ Error 4: Confundir patrones similares

Strategy y State tienen estructuras casi idénticas (interfaz + implementaciones intercambiables), pero su intención es diferente. Strategy permite al cliente elegir el algoritmo; State cambia el comportamiento internamente al objeto según su estado. Confundirlos lleva a diseños que se entienden mal y se mantienen peor.

📝 Ejercicios prácticos

Ejercicio 1: Singleton para Logger (Básico)

Implementa una clase Logger usando el patrón Singleton (idiom de Bill Pugh) que permita registrar mensajes con tres niveles: INFO, WARNING y ERROR. Cada mensaje debe imprimirse con la marca de tiempo actual. Demuestra que todas las llamadas a Logger.getInstancia() devuelven el mismo objeto.

Ver solución
Logger.java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class Logger {

    private static final DateTimeFormatter FORMATO =
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    private Logger() { }

    private static class Holder {
        private static final Logger INSTANCIA = new Logger();
    }

    public static Logger getInstancia() {
        return Holder.INSTANCIA;
    }

    public void info(String mensaje) {
        log("INFO", mensaje);
    }

    public void warning(String mensaje) {
        log("WARNING", mensaje);
    }

    public void error(String mensaje) {
        log("ERROR", mensaje);
    }

    private void log(String nivel, String mensaje) {
        String timestamp = LocalDateTime.now().format(FORMATO);
        System.out.printf("[%s] [%s] %s%n", timestamp, nivel, mensaje);
    }

    // Demostración
    public static void main(String[] args) {
        Logger log1 = Logger.getInstancia();
        Logger log2 = Logger.getInstancia();

        System.out.println("¿Misma instancia? " + (log1 == log2)); // true

        log1.info("Sistema iniciado");
        log2.warning("Memoria al 85%");
        log1.error("Conexión a BD perdida");
    }
}

Ejercicio 2: Factory Method para formas geométricas (Intermedio)

Diseña un sistema de dibujo usando Factory Method. Crea una interfaz Forma con métodos dibujar() y calcularArea(). Implementa al menos tres formas concretas: Circulo, Rectangulo y Triangulo. La clase abstracta FabricaFormas debe tener un Factory Method crearForma(). Demuestra que añadir una nueva forma (por ejemplo, Hexagono) no requiere modificar las clases existentes.

Ver solución
FormasFactory.java
// Producto
interface Forma {
    void dibujar();
    double calcularArea();
}

class Circulo implements Forma {
    private double radio;
    public Circulo(double radio) { this.radio = radio; }

    @Override
    public void dibujar() {
        System.out.println("Dibujando círculo de radio " + radio);
    }

    @Override
    public double calcularArea() {
        return Math.PI * radio * radio;
    }
}

class Rectangulo implements Forma {
    private double ancho, alto;
    public Rectangulo(double ancho, double alto) {
        this.ancho = ancho;
        this.alto = alto;
    }

    @Override
    public void dibujar() {
        System.out.printf("Dibujando rectángulo %sx%s%n", ancho, alto);
    }

    @Override
    public double calcularArea() {
        return ancho * alto;
    }
}

// Creador abstracto
abstract class FabricaFormas {
    protected abstract Forma crearForma();

    public void generarForma() {
        Forma forma = crearForma();
        forma.dibujar();
        System.out.printf("Área: %.2f%n", forma.calcularArea());
    }
}

// Creadores concretos
class FabricaCirculo extends FabricaFormas {
    private double radio;
    public FabricaCirculo(double radio) { this.radio = radio; }

    @Override
    protected Forma crearForma() { return new Circulo(radio); }
}

class FabricaRectangulo extends FabricaFormas {
    private double ancho, alto;
    public FabricaRectangulo(double ancho, double alto) {
        this.ancho = ancho;
        this.alto = alto;
    }

    @Override
    protected Forma crearForma() { return new Rectangulo(ancho, alto); }
}

// Demostración
public class FormasDemo {
    public static void main(String[] args) {
        FabricaFormas[] fabricas = {
            new FabricaCirculo(5.0),
            new FabricaRectangulo(4.0, 6.0)
        };

        for (FabricaFormas f : fabricas) {
            f.generarForma();
            System.out.println("---");
        }
    }
}

Ejercicio 3: Observer + Strategy para sistema de alertas (Avanzado)

Diseña un sistema de monitorización de sensores de temperatura para un centro de datos. El sistema debe utilizar el patrón Observer para que múltiples monitores (pantalla, log, alarma sonora) se suscriban a las lecturas de un sensor. Además, usa Strategy para definir diferentes políticas de alerta: «conservadora» (alerta a partir de 70°C), «moderada» (alerta a partir de 80°C) y «agresiva» (alerta a partir de 90°C). La política debe ser intercambiable en tiempo de ejecución.

Ver solución
SistemaAlertas.java
import java.util.ArrayList;
import java.util.List;

// Strategy: política de alerta
@FunctionalInterface
interface PoliticaAlerta {
    boolean debeAlertar(double temperatura);
}

class PoliticaConservadora implements PoliticaAlerta {
    @Override
    public boolean debeAlertar(double temp) { return temp >= 70.0; }
}

class PoliticaModerada implements PoliticaAlerta {
    @Override
    public boolean debeAlertar(double temp) { return temp >= 80.0; }
}

// Observer: monitores
interface MonitorTemperatura {
    void actualizar(String sensorId, double temperatura, boolean alerta);
}

class MonitorPantalla implements MonitorTemperatura {
    @Override
    public void actualizar(String sensorId, double temp, boolean alerta) {
        String estado = alerta ? "🔴 ALERTA" : "🟢 Normal";
        System.out.printf("[PANTALLA] %s: %.1f°C → %s%n",
            sensorId, temp, estado);
    }
}

class MonitorLog implements MonitorTemperatura {
    @Override
    public void actualizar(String sensorId, double temp, boolean alerta) {
        System.out.printf("[LOG] Sensor=%s, Temp=%.1f, Alerta=%s%n",
            sensorId, temp, alerta);
    }
}

// Sujeto observable
class SensorTemperatura {
    private String id;
    private double temperatura;
    private List<MonitorTemperatura> monitores = new ArrayList<>();
    private PoliticaAlerta politica;

    public SensorTemperatura(String id, PoliticaAlerta politica) {
        this.id = id;
        this.politica = politica;
    }

    public void agregarMonitor(MonitorTemperatura m) {
        monitores.add(m);
    }

    public void setPolitica(PoliticaAlerta politica) {
        this.politica = politica;
    }

    public void registrarLectura(double nuevaTemp) {
        this.temperatura = nuevaTemp;
        boolean alerta = politica.debeAlertar(nuevaTemp);
        for (MonitorTemperatura m : monitores) {
            m.actualizar(id, nuevaTemp, alerta);
        }
    }
}

// Demostración
public class SistemaAlertas {
    public static void main(String[] args) {
        SensorTemperatura sensor = new SensorTemperatura(
            "RACK-01", new PoliticaConservadora());

        sensor.agregarMonitor(new MonitorPantalla());
        sensor.agregarMonitor(new MonitorLog());

        System.out.println("=== Política CONSERVADORA (alerta >= 70°C) ===");
        sensor.registrarLectura(65.0);
        sensor.registrarLectura(72.0);

        System.out.println("\n=== Cambio a política MODERADA (alerta >= 80°C) ===");
        sensor.setPolitica(new PoliticaModerada());
        sensor.registrarLectura(72.0);  // Ahora no alerta
        sensor.registrarLectura(85.0);
    }
}

❓ Preguntas frecuentes

¿Cuántos patrones de diseño existen en el catálogo GoF?

El catálogo original del Gang of Four (GoF) define 23 patrones de diseño, clasificados en tres categorías: 5 creacionales (Singleton, Factory Method, Abstract Factory, Builder, Prototype), 7 estructurales (Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy) y 11 de comportamiento (Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor).

¿Cuál es la diferencia entre Factory Method y Abstract Factory?

Factory Method define una interfaz para crear un solo objeto, delegando la decisión a las subclases. Abstract Factory proporciona una interfaz para crear familias de objetos relacionados sin especificar sus clases concretas. Factory Method usa herencia (una subclase decide qué crear), mientras que Abstract Factory usa composición (un objeto fábrica crea toda una familia de productos).

¿Es Singleton un antipatrón?

Singleton no es inherentemente un antipatrón, pero su mal uso es muy común. Los problemas surgen cuando se usa como variable global encubierta, cuando dificulta las pruebas unitarias o cuando introduce acoplamiento innecesario. En Java moderno, se recomienda usar inyección de dependencias con frameworks como Spring en lugar de Singletons manuales para la mayoría de casos.

¿Qué patrón de diseño debo aprender primero?

Los patrones más útiles para empezar son Singleton (el más sencillo de entender), Observer (fundamental para eventos y arquitecturas reactivas), Strategy (para entender cómo encapsular algoritmos) y Factory Method (para comprender la delegación de creación). Estos cuatro cubren las tres categorías y aparecen constantemente en proyectos reales y en la API estándar de Java.

¿Los patrones de diseño son exclusivos de Java?

No, los patrones de diseño son independientes del lenguaje de programación. Son soluciones conceptuales a problemas recurrentes de diseño orientado a objetos. Sin embargo, su implementación varía según el lenguaje: en Java se usan interfaces y clases abstractas, mientras que en lenguajes como Python o JavaScript pueden implementarse de formas diferentes aprovechando las características propias de cada lenguaje.

¿Dónde se usan los patrones de diseño en la API de Java?

La API estándar de Java está repleta de patrones: Iterator (java.util.Iterator), Observer (java.util.Observer, PropertyChangeListener), Factory Method (Calendar.getInstance(), NumberFormat.getInstance()), Decorator (clases I/O como BufferedReader envolviendo FileReader), Adapter (Arrays.asList()), Singleton (Runtime.getRuntime()), Strategy (Comparator), Proxy (java.lang.reflect.Proxy) y Template Method (HttpServlet.doGet/doPost), entre muchos otros.

¿Cuándo NO debo usar un patrón de diseño?

No debes usar un patrón cuando añade complejidad innecesaria al problema. Si tu diseño es sencillo y funciona bien, forzar un patrón es sobreingeniería. Tampoco conviene aplicar patrones preventivamente "por si acaso" el sistema crece. La regla práctica es: aplica un patrón cuando el problema que resuelve ya se ha manifestado en tu código, no antes.

❓ Preguntas frecuentes sobre Patrones de Diseño en Java

Las dudas más comunes respondidas de forma clara y directa.

El catálogo original del Gang of Four (GoF) define 23 patrones de diseño, clasificados en tres categorías: 5 creacionales (Singleton, Factory Method, Abstract Factory, Builder, Prototype), 7 estructurales (Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy) y 11 de comportamiento (Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor).
Factory Method define una interfaz para crear un solo objeto, delegando la decisión a las subclases. Abstract Factory proporciona una interfaz para crear familias de objetos relacionados sin especificar sus clases concretas. Factory Method usa herencia (una subclase decide qué crear), mientras que Abstract Factory usa composición (un objeto fábrica crea toda una familia de productos).
Singleton no es inherentemente un antipatrón, pero su mal uso es muy común. Los problemas surgen cuando se usa como variable global encubierta, cuando dificulta las pruebas unitarias o cuando introduce acoplamiento innecesario. En Java moderno, se recomienda usar inyección de dependencias con frameworks como Spring en lugar de Singletons manuales para la mayoría de casos.
Los patrones más útiles para empezar son Singleton (el más sencillo de entender), Observer (fundamental para eventos y arquitecturas reactivas), Strategy (para entender cómo encapsular algoritmos) y Factory Method (para comprender la delegación de creación). Estos cuatro cubren las tres categorías y aparecen constantemente en proyectos reales y en la API estándar de Java.
No, los patrones de diseño son independientes del lenguaje de programación. Son soluciones conceptuales a problemas recurrentes de diseño orientado a objetos. Sin embargo, su implementación varía según el lenguaje: en Java se usan interfaces y clases abstractas, mientras que en lenguajes como Python o JavaScript pueden implementarse de formas diferentes aprovechando las características propias de cada lenguaje.
La API estándar de Java está repleta de patrones: Iterator (java.util.Iterator), Observer (java.util.Observer, PropertyChangeListener), Factory Method (Calendar.getInstance(), NumberFormat.getInstance()), Decorator (clases I/O como BufferedReader envolviendo FileReader), Adapter (Arrays.asList()), Singleton (Runtime.getRuntime()), Strategy (Comparator), Proxy (java.lang.reflect.Proxy) y Template Method (HttpServlet.doGet/doPost), entre muchos otros.
No debes usar un patrón cuando añade complejidad innecesaria al problema. Si tu diseño es sencillo y funciona bien, forzar un patrón es sobreingeniería. Tampoco conviene aplicar patrones preventivamente "por si acaso" el sistema crece. La regla práctica es: aplica un patrón cuando el problema que resuelve ya se ha manifestado en tu código, no antes.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Patrones de Diseño en Java? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!