Esta lección salda la deuda más antigua del curso. En la primera lección viste un Pedido.cambiarEstado(...) que, además de cambiar el estado, creaba con new un ServicioPush, un ServicioSms y un PanelEstadisticas y los llamaba uno a uno. Dijimos entonces: "existe una estructura clásica que resuelve exactamente esto... y tiene nombre". Tres módulos después, aquí está. Observer invierte la relación: el pedido deja de conocer a sus interesados y son los interesados quienes se suscriben al pedido. Uno cambia, muchos se enteran, nadie conoce a nadie. Es probablemente el patrón de comportamiento más influyente del catálogo — de él descienden los listeners de toda UI, los eventos de los frameworks y, en última instancia, las arquitecturas de eventos que veremos en el módulo 6.

Contenido

  1. La deuda de la lección 01-01, releída con ojos de módulo 4
  2. Intención y estructura del patrón
  3. Implementación Java completa
  4. Push vs. pull: cuánto contar en la notificación
  5. Detalles que muerden: orden, errores y fugas de listeners
  6. Observer en el JDK y en las UI
  7. De Observer a eventos y pub-sub (la puerta al módulo 6)
  8. Cuándo usarlo y cuándo no
  9. Relación con otros patrones
  10. Errores comunes
  11. Ejercicios y conclusión

La deuda de la lección 01-01, releída con ojos de módulo 4

El código del crimen, tal como lo dejamos en la primera lección:

public void cambiarEstado(String nuevoEstado) {
    this.estado = nuevoEstado;

    ServicioPush push = new ServicioPush();
    push.enviar(this.getCliente().getTokenDispositivo(), "Tu pedido está: " + nuevoEstado);

    ServicioSms sms = new ServicioSms();
    sms.enviar(this.getRepartidor().getTelefono(), "Pedido " + this.getId() + ": " + nuevoEstado);

    PanelEstadisticas panel = new PanelEstadisticas();
    panel.registrarCambioEstado(this.getId(), nuevoEstado);
}

Entonces solo olía mal; ahora puedes nombrar cada delito con lo aprendido:

  • Los new cableados: el problema creacional que el módulo 2 desmontó — imposible testear sin enviar SMS reales.
  • Pedido conoce los detalles de push, SMS y estadísticas: violación de DIP y SRP a la vez.
  • Cada interesado nuevo (email, fidelización, cocina) modifica Pedido: violación frontal de OCP.
  • El número de interesados es variable y creciente — la definición misma del contexto que pide este patrón: un objeto de negocio cuyos cambios interesan a un número variable de terceros que él no debería conocer.

La inversión que lo arregla: Pedido no llama a sus interesados; mantiene una lista de suscriptores anónimos y, cuando cambia, los recorre avisando a través de una interfaz mínima. Quién está en la lista se decide fuera, en configuración — y puede cambiar en ejecución.

Intención y estructura del patrón

Intención (GoF): definir una dependencia uno-a-muchos entre objetos, de modo que cuando un objeto cambia de estado, todos sus dependientes son notificados y actualizados automáticamente.

classDiagram
    class ObservadorPedido {
        <<interface>>
        +pedidoActualizado(pedido: Pedido, anterior: EstadoPedido)
    }
    class Pedido {
        -observadores: List~ObservadorPedido~
        -estado: EstadoPedido
        +suscribir(o: ObservadorPedido)
        +desuscribir(o: ObservadorPedido)
        +cambiarEstado(nuevo: EstadoPedido)
        -notificarObservadores(anterior: EstadoPedido)
    }
    class NotificadorCliente {
        -notificador: Notificador
        +pedidoActualizado(pedido, anterior)
    }
    class NotificadorRepartidor {
        +pedidoActualizado(pedido, anterior)
    }
    class MonitorCocina {
        +pedidoActualizado(pedido, anterior)
    }
    class PanelEstadisticas {
        +pedidoActualizado(pedido, anterior)
    }

    ObservadorPedido <|.. NotificadorCliente
    ObservadorPedido <|.. NotificadorRepartidor
    ObservadorPedido <|.. MonitorCocina
    ObservadorPedido <|.. PanelEstadisticas
    Pedido o-- ObservadorPedido : notifica a
Rol GoF En PideYa
Subject (mantiene suscriptores y notifica) Pedido
Observer (interfaz mínima de aviso) ObservadorPedido
ConcreteObserver NotificadorCliente, NotificadorRepartidor, MonitorCocina, PanelEstadisticas

Lo esencial está en la dirección de las flechas: Pedido depende solo de la interfaz ObservadorPedido — nunca de un observador concreto. Los concretos dependen de Pedido (leen sus datos), pero Pedido no sabe ni cuántos son ni qué hacen. La dependencia uno-a-muchos existe en ejecución (la lista), no en compilación.

sequenceDiagram
    participant F as FachadaCheckout
    participant P as Pedido (Subject)
    participant NC as NotificadorCliente
    participant NR as NotificadorRepartidor
    participant PE as PanelEstadisticas
    F->>P: cambiarEstado(EN_REPARTO)
    P->>P: estado = EN_REPARTO
    P->>NC: pedidoActualizado(this, PAGADO)
    NC->>NC: push al cliente
    P->>NR: pedidoActualizado(this, PAGADO)
    NR->>NR: SMS al repartidor
    P->>PE: pedidoActualizado(this, PAGADO)
    PE->>PE: registrar métrica
    Note over P: Pedido no sabe qué hizo cada uno

Implementación Java completa

La interfaz Observer. Un método, bien nombrado por el acontecimiento. Pasamos el pedido y el estado anterior — la elección push/pull la discutimos en la sección siguiente:

public interface ObservadorPedido {
    void pedidoActualizado(Pedido pedido, EstadoPedido estadoAnterior);
}

El Subject. El cambiarEstado redimido, tres módulos después:

public class Pedido {

    private final List<ObservadorPedido> observadores = new CopyOnWriteArrayList<>();
    private EstadoPedido estado = EstadoPedido.CREADO;

    public void suscribir(ObservadorPedido observador) {
        observadores.add(Objects.requireNonNull(observador));
    }

    public void desuscribir(ObservadorPedido observador) {
        observadores.remove(observador);
    }

    public void cambiarEstado(EstadoPedido nuevo) {
        if (nuevo == this.estado) {
            return;                       // sin cambio real, sin ruido
        }
        EstadoPedido anterior = this.estado;
        this.estado = nuevo;              // 1º consolidar el estado...
        notificarObservadores(anterior);  // ...2º avisar
    }

    private void notificarObservadores(EstadoPedido anterior) {
        for (ObservadorPedido o : observadores) {
            try {
                o.pedidoActualizado(this, anterior);
            } catch (RuntimeException e) {
                // Un observador roto no puede tumbar la notificación de los demás
                RegistroEventos.INSTANCIA.error("Observador falló: " + o.getClass(), e);
            }
        }
    }
}

Decisiones comentadas, todas con cicatriz detrás:

  • CopyOnWriteArrayList: permite que un observador se desuscriba durante una notificación (o que otro hilo se suscriba) sin ConcurrentModificationException. Para listas de suscriptores —muchas lecturas, pocas escrituras— es la estructura idónea.
  • Estado primero, aviso después: los observadores leerán pedido.getEstado(); si notificas antes de consolidar, leen el pasado.
  • try/catch por observador: sin él, el primer observador que lance excepción deja a los demás sin su aviso — y el fallo del panel de estadísticas no debe impedir el SMS al repartidor.
  • Filtro de no-cambio: notificar "cambios" que no cambian nada genera cascadas de ruido (y, en grafos de observadores, bucles).

Los observadores concretos. Cada uno, una clase pequeña con una sola razón de cambio — y reutilizando piezas del curso: el Notificador del Factory Method, quizá envuelto en el NotificadorConReintentos del Decorator:

public class NotificadorCliente implements ObservadorPedido {

    private final Notificador notificador;   // inyectado: push, sms, email... da igual

    public NotificadorCliente(Notificador notificador) {
        this.notificador = notificador;
    }

    @Override
    public void pedidoActualizado(Pedido pedido, EstadoPedido anterior) {
        notificador.enviar("Tu pedido " + pedido.getId() + " está: " + pedido.getEstado());
    }
}

public class PanelEstadisticas implements ObservadorPedido {
    @Override
    public void pedidoActualizado(Pedido pedido, EstadoPedido anterior) {
        metricas.registrarTransicion(anterior, pedido.getEstado());
    }
}

El montaje, en configuración (la FachadaCheckout al crear el pedido, o el arranque de la app):

pedido.suscribir(new NotificadorCliente(registroNotificadores.crear("push")));
pedido.suscribir(new NotificadorRepartidor(registroNotificadores.crear("sms")));
pedido.suscribir(new MonitorCocina(pantallaCocina));
pedido.suscribir(new PanelEstadisticas(metricas));

Pasa revista a los delitos de 01-01: los new de servicios, fuera de Pedido (y testear es suscribir un observador falso); los detalles de push/SMS, en sus observadores; el interesado nuevo —fidelización que suma puntos al llegar a ENTREGADO— es una clase nueva y una línea de suscripción: Pedido no se toca. Deuda saldada.

Push vs. pull: cuánto contar en la notificación

La firma del método de aviso admite un espectro:

Modelo Firma típica Ventaja Inconveniente
Push puro pedidoActualizado(id, nuevoEstado, horaEstimada, zona...) El observador no toca al subject; puede ni conocerlo El subject adivina qué necesita cada cual; firmas que crecen
Pull puro pedidoActualizado() Firma mínima y estable Todos vuelven al subject a preguntar; el estado puede haber vuelto a cambiar entre aviso y consulta
Híbrido pedidoActualizado(pedido, estadoAnterior) Referencia para consultar + el dato clave del evento —

Elegimos el híbrido con motivo: el estadoAnterior solo existe en el momento del aviso (después, nadie lo recuerda — el panel de estadísticas lo necesita para registrar transiciones), y la referencia al pedido permite a cada observador leer lo que le interese sin engordar la firma. Regla práctica: empuja lo efímero del evento, deja en pull lo consultable.

Detalles que muerden: orden, errores y fugas de listeners

Orden de notificación. Nuestro subject notifica en orden de suscripción, pero eso es un detalle de implementación: el contrato de Observer es "todos se enteran", no "en este orden". El error grave es diseñar observadores que dependen del orden (el SMS asume que estadísticas ya registró): eso son dependencias ocultas entre supuestos desconocidos, invisibles en el código de montaje. Si el orden importa de verdad, Observer es el patrón equivocado — quieres una orquestación explícita (Facade llamando pasos en orden, o un Mediator dirigiendo).

Errores. Ya visto en el código: aislar cada observador con try/catch. La pregunta de diseño es qué más hacer — registrar y seguir (nuestra opción), reintentar (el decorador de reintentos del módulo 3 lo hace por nosotros en los notificadores), o desuscribir al reincidente.

Fugas de listeners. El bug de producción más famoso del patrón: el subject guarda una referencia fuerte a cada observador, así que un observador que olvida desuscribirse no puede ser recolectado mientras viva el subject — y sigue recibiendo (y procesando) avisos de una pantalla que el usuario cerró hace una hora. En PideYa: la pantalla de seguimiento se suscribe al pedido; el usuario navega atrás; la pantalla "muerta" sigue colgada del pedido hasta que este se entrega. Antídotos: desuscribir simétricamente en el ciclo de vida (al cerrar la pantalla), y para suscriptores de vida corta sobre subjects de vida larga, referencias débiles (WeakReference) que permitan al recolector llevárselos.

Observer en el JDK y en las UI

El patrón empapa la plataforma:

  • Los listeners de UI (ActionListener de Swing, los listeners de Android/JavaFX, los addEventListener del DOM): cada botón es un subject; cada onClick, un observador. Toda la programación de interfaces es Observer industrializado.
  • PropertyChangeListener/PropertyChangeSupport (java.beans): un subject reutilizable por composición — tu clase delega en PropertyChangeSupport la lista y la notificación, en vez de reimplementarlas.
  • java.util.Observable/Observer: el soporte original del JDK, deprecado desde Java 9 — clase en vez de interfaz (te robaba la herencia), sin tipos genéricos, sin orden garantizado. Su lección póstuma: mejor una interfaz propia de dominio (nuestro ObservadorPedido) que un observador genérico de talla única.
  • java.util.concurrent.Flow (Java 9+): la versión moderna con contrapresión (el suscriptor pide cuántos elementos puede procesar), base de la programación reactiva (RxJava, Reactor).

De Observer a eventos y pub-sub (la puerta al módulo 6)

Una generalización natural, solo apuntada: si en vez de suscribirse a un pedido concreto los interesados se suscriben a tipos de evento (PedidoEntregado, PedidoCancelado) en un intermediario —un bus de eventos—, subject y observadores dejan de conocerse incluso como interfaces: publicas un evento y quien esté suscrito al tipo lo recibe. Eso es publicar-suscribir (pub-sub); a escala de procesos y con un broker (Kafka, RabbitMQ) por medio, es la base de las arquitecturas orientadas a eventos con las que PideYa integrará cocina, reparto y facturación en el módulo 6. Todo ese mundo es este patrón con la lista de suscriptores externalizada. Aquí lo dejamos: mención hecha, desarrollo allí.

Cuándo usarlo y cuándo no

Úsalo cuando:

  • Un cambio en un objeto interesa a otros cuyo número e identidad varían (en configuración o en ejecución).
  • El objeto observado no debe conocer a sus interesados (módulos independientes, capas que no deben mirarse).
  • Quieres poder añadir reacciones nuevas sin tocar al que cambia (OCP).

Evítalo cuando:

  • Hay un solo interesado, fijo y para siempre: una llamada directa es más clara y depurable.
  • Las reacciones deben ocurrir en orden garantizado o en transacción con el cambio (todo o nada): la difusión desordenada y sin acuse de recibo no da esas garantías; orquesta explícitamente.
  • La cadena causa-efecto ya es difícil de seguir: el gran coste de Observer es que el flujo se vuelve invisible — mirando cambiarEstado nadie sabe qué ocurrirá, hay que rastrear las suscripciones. En sistemas saturados de observadores, depurar es arqueología. Buen logging de suscripción/notificación (via RegistroEventos) es oxígeno.

Relación con otros patrones

  • Mediator: el careo estrella del módulo (frente a frente en la comparativa): Observer difunde sin protocolo central; Mediator dirige con protocolo. Y se combinan: un mediador puede enterarse de las cosas observando a sus colegas.
  • State (siguiente lección): socios directos — State decidirá qué transiciones son legales en el pedido; Observer difunde las que ocurren.
  • Singleton: los subjects globales (un bus de eventos como singleton) heredan todos los males del estado global del módulo 2.
  • Decorator: los observadores notificadores reutilizan los decoradores técnicos (reintentos, logging) sin que el subject lo sepa.
  • Command: los avisos pueden materializarse como comandos encolados — primer paso hacia la notificación asíncrona.

Errores comunes

  • Olvidar desuscribir: la fuga de listeners ya descrita; simetría suscribir/desuscribir en el ciclo de vida, siempre.
  • Trabajo pesado en el hilo del aviso: nuestro bucle es síncrono — un observador que tarda 3 segundos congela cambiarEstado (y en UI, la interfaz entera). Lo lento, a una cola o executor (el aviso encolado como comando).
  • Observadores que mutan al subject durante el aviso: pedidoActualizado que llama a pedido.cambiarEstado(...) provoca notificaciones reentrantes y bucles. Si una reacción debe causar otra transición, que lo pida después (encolando), no dentro del aviso.
  • Depender del orden de notificación entre observadores: dependencias ocultas; ya explicado.
  • Notificar con el lock puesto (en subjects sincronizados): receta de interbloqueo si un observador vuelve a entrar al subject. Copia la lista y notifica fuera de la sección crítica (la CopyOnWriteArrayList te lo regala).
  • Tragarse las excepciones sin registrarlas: aislar no es silenciar; el catch sin log convierte fallos reales en misterios.

Ejercicios

Ejercicio 1: el observador de fidelización

Implementa ProgramaFidelizacion implements ObservadorPedido: cuando un pedido llega a ENTREGADO, suma al cliente 1 punto por cada euro del total (servicioPuntos.sumar(clienteId, puntos)). Los demás estados no le interesan. ¿Qué tuviste que tocar en Pedido?

Ejercicio 2: cazar la fuga

La pantalla PantallaSeguimiento se suscribe al pedido en su constructor. Los usuarios se quejan de que la app va cada vez más lenta durante pedidos largos, y el perfilador muestra cientos de instancias de PantallaSeguimiento vivas. Explica la causa exacta (¿quién retiene a quién?) y escribe la corrección.

Ejercicio 3: ¿Observer o llamada directa?

Decide y justifica: (a) al confirmar el pedido, cobrar en la pasarela de pago; (b) al cambiar el estado, refrescar la pantalla de cocina; (c) al entregar, generar la factura, que legalmente debe existir antes de dar el pedido por cerrado; (d) al cambiar el precio de un plato, invalidar el ProxyCacheCatalogo del módulo 3.

Soluciones

Solución 1:

public class ProgramaFidelizacion implements ObservadorPedido {

    private final ServicioPuntos servicioPuntos;

    public ProgramaFidelizacion(ServicioPuntos servicioPuntos) {
        this.servicioPuntos = servicioPuntos;
    }

    @Override
    public void pedidoActualizado(Pedido pedido, EstadoPedido anterior) {
        if (pedido.getEstado() != EstadoPedido.ENTREGADO) {
            return;                                  // filtra: solo le interesa entregar
        }
        int puntos = pedido.getTotal().intValue();   // 1 punto por euro
        servicioPuntos.sumar(pedido.getCliente().getId(), puntos);
    }
}

En Pedido: nada. Una clase nueva y una línea de suscripción en el montaje. Compáralo con la versión de 01-01, donde esto habría sido el cuarto bloque cableado dentro de cambiarEstado — este ejercicio es la demostración de la deuda saldada.

Solución 2: el pedido (subject, vivo durante horas) guarda referencia fuerte a cada PantallaSeguimiento en su lista de observadores; el usuario abre y cierra la pantalla veinte veces, y ninguna instancia puede ser recolectada porque el pedido las retiene — y las veinte siguen procesando cada aviso. Corrección: desuscripción simétrica en el ciclo de vida de la pantalla:

public class PantallaSeguimiento implements ObservadorPedido {
    private final Pedido pedido;

    public PantallaSeguimiento(Pedido pedido) {
        this.pedido = pedido;
        pedido.suscribir(this);          // al abrir
    }

    public void alCerrar() {             // hook de ciclo de vida de la UI
        pedido.desuscribir(this);        // simetría obligatoria
    }
    // ...
}

(Defensa adicional si el ciclo de vida no es fiable: que el subject guarde WeakReference<ObservadorPedido> y purgue las muertas al notificar.)

Solución 3: (a) llamada directa (orquestada por la FachadaCheckout): el cobro es un paso esencial, síncrono y con manejo de errores propio del flujo — no una "reacción opcional"; (b) Observer: la cocina es un interesado más entre varios, sin orden ni transacción — MonitorCocina de manual; (c) llamada directa/orquestación: hay una garantía de orden y atomicidad ("antes de cerrar") que Observer no ofrece por contrato; (d) Observer: la caché es un interesado técnico que ni debe conocer el catálogo de negocio ni ser conocido — y mañana pueden sumarse el buscador y el histórico de precios (los interesados variables de la definición).

Conclusión

La deuda de la primera lección está saldada, y con intereses: Pedido.cambiarEstado(...) ya no crea ni conoce a nadie — mantiene una lista de ObservadorPedido anónimos, consolida su estado, y difunde el aviso aislando fallos; cliente, repartidor, cocina, estadísticas y el recién llegado programa de fidelización reaccionan cada uno en su clase, enchufados y desenchufados en configuración. Te llevas además el oficio que separa el diagrama del patrón en producción: híbrido push/pull con el estado anterior, no depender del orden, aislar excepciones, y la caza de la fuga de listeners. Y una puerta entreabierta: externaliza la lista de suscriptores a un bus y tienes pub-sub — el módulo 6 espera.

Pero fíjate en lo que Observer no vigila: hoy nada impide cambiarEstado(ENTREGADO) sobre un pedido recién creado y sin pagar — avisaríamos a todo el mundo, eso sí, de una barbaridad. Qué transiciones son legales, y cómo cambia el comportamiento del pedido según dónde esté en su ciclo de vida —qué se puede cancelar, qué se puede modificar—, es un problema distinto: el comportamiento que cambia con el estado interno. Nos vemos en State.

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