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
- La deuda de la lección 01-01, releída con ojos de módulo 4
- Intención y estructura del patrón
- Implementación Java completa
- Push vs. pull: cuánto contar en la notificación
- Detalles que muerden: orden, errores y fugas de listeners
- Observer en el JDK y en las UI
- De Observer a eventos y pub-sub (la puerta al módulo 6)
- Cuándo usarlo y cuándo no
- Relación con otros patrones
- Errores comunes
- 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
newcableados: el problema creacional que el módulo 2 desmontó — imposible testear sin enviar SMS reales. Pedidoconoce 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) sinConcurrentModificationException. 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/catchpor 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 (
ActionListenerde Swing, los listeners de Android/JavaFX, losaddEventListenerdel DOM): cada botón es un subject; cadaonClick, un observador. Toda la programación de interfaces es Observer industrializado. PropertyChangeListener/PropertyChangeSupport(java.beans): un subject reutilizable por composición — tu clase delega enPropertyChangeSupportla 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 (nuestroObservadorPedido) 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
cambiarEstadonadie sabe qué ocurrirá, hay que rastrear las suscripciones. En sistemas saturados de observadores, depurar es arqueología. Buen logging de suscripción/notificación (viaRegistroEventos) 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:
pedidoActualizadoque llama apedido.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
CopyOnWriteArrayListte lo regala). - Tragarse las excepciones sin registrarlas: aislar no es silenciar; el
catchsin 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
- ¿Qué son los Patrones de Diseño?
- Historia y Origen de los Patrones de Diseño
- Principios de Diseño: SOLID y Otros Fundamentos
- UML Esencial para Entender Patrones
- Clasificación de los Patrones de Diseño
- Ventajas y Desventajas de Usar Patrones de Diseño
Módulo 2: Patrones Creacionales
- Introducción a los Patrones Creacionales
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa y Elección de Patrones Creacionales
Módulo 3: Patrones Estructurales
- Introducción a los Patrones Estructurales
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa y Elección de Patrones Estructurales
Módulo 4: Patrones de Comportamiento
- Introducción a los Patrones de Comportamiento
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa y Elección de Patrones de Comportamiento
Módulo 5: Aplicación de Patrones de Diseño
- Cómo Seleccionar el Patrón Adecuado
- Ejemplos Prácticos de Uso de Patrones
- Patrones de Diseño en Proyectos Reales
- Refactorización Usando Patrones de Diseño
- Antipatrones: Cuándo los Patrones se Vuelven un Problema
Módulo 6: Patrones de Diseño Avanzados
- Patrones de Diseño en Arquitecturas Modernas
- Patrones de Diseño en Microservicios
- Patrones de Diseño en Sistemas Distribuidos
- Patrones de Concurrencia
- Patrones de Diseño en Desarrollo Ágil
