Observer Pattern

Intención y problema

¿Cómo permitir que múltiples dependientes reaccionen a cambios sin que el publisher conozca los detalles concretos de cada uno? Observer organiza una dependencia uno-a-muchos mediante registro y notificación a un contrato compartido.

Pertenece a behavioral-patterns. La síntesis GoF de CMU sobre Observer destaca mantener dependientes sincronizados sin fijar todos los detalles del mecanismo. El editor usado aquí es un ejemplo propio para examinar esas decisiones.

Un DocumentModel cambia mientras una preview, un indicador de modificaciones y un resumen muestran información relacionada. Si el modelo llama a clases concretas de cada pantalla, conoce detalles de presentación y debe cambiar al agregar otro interesado. Una conversación común permite incorporar o retirar dependientes sin agregar ese conocimiento concreto al publisher.

Forces y alcance

Queremos desacoplar interesados, pero también conservar un flujo explicable. Observer reduce conocimiento de tipos concretos; mantiene dependencias sobre señales, estado observable y políticas de notificación. No elimina todo coupling.

Otra fuerza es actualizar dependientes oportunamente sin convertir un cambio pequeño en una cascada costosa. La frecuencia, el tamaño de datos y la cantidad de interesados afectan el diseño. Notificar cada modificación interna puede revelar detalles que no constituyen un cambio significativo para el consumidor.

Debe definirse qué significa una señal: estado disponible, cambio confirmado o invalidación que exige volver a consultar. Registrar una función sin esa semántica permite callbacks, pero no asegura una colaboración correcta.

Estructura y colaboración

DocumentModel                  Subject / Publisher
    ├── register(observer)
    ├── unsubscribe(observer)
    └── notify(change)
            ↓ Observer contract
            ├── Preview
            ├── ModificationIndicator
            └── Summary

Subject conserva o accede al registro de interesados y publica cambios. Observer define cómo recibe una actualización. Los observers concretos realizan sus reacciones; el publisher no necesita conocer las clases de pantalla ni cómo renderizan.

Una colaboración posible es crear pantalla → registrar interesado → modificar modelo → establecer estado consistente → notificar → actualizar presentación → retirar suscripción al cerrar. Esta secuencia es una decisión del ejemplo, no una garantía universal del patrón.

El propietario de la suscripción puede ser el observer, un componente que lo ensambla o un contenedor de lifecycle. Debe ser explícito. También hay que acordar qué sucede si alguien se registra o se retira durante una notificación: snapshot del registro, efecto inmediato o cambio para el próximo ciclo son políticas posibles.

Push y Pull

En Push, el publisher proporciona información:

notify(change)

En Pull, informa que existe una actualización y el observer consulta lo relevante:

notify()
    → observer reads subject
VarianteVentaja posibleObligación o costo
PushLlevar los datos relevantes del cambio.Definir payload, tamaño y dependencia sobre su significado.
PullCada interesado elige qué consultar.Definir estado visible, costo de lectura y consistencia temporal.

Push puede entregar un snapshot del cambio si así se construye el payload; enviar una referencia mutable no demuestra snapshot. Un payload grande puede transportar información que muchos interesados no usan, mientras uno demasiado específico obliga al publisher a conocer necesidades particulares.

Pull conserva flexibilidad para consumidores con intereses diferentes, pero los acopla al contrato consultable del Subject. Cuando consultan, el estado puede haber cambiado otra vez: leen el estado actual, no necesariamente el que produjo la señal. Necesitar el cambio exacto puede exigir una versión o datos adicionales.

Ninguna variante es universalmente mejor. Hay que decidir si interesa reconstruir cambios o refrescar una vista del estado reciente, y qué costo resulta aceptable. Las formas híbridas también necesitan esa decisión, aunque combinen señal y datos mínimos.

Lifetime: subscribe sin unsubscribe

Una pantalla de vida corta puede suscribirse a un modelo que vive toda la sesión. Si el registro mantiene una referencia fuerte al callback y este captura la pantalla, cerrar su vista no libera necesariamente el objeto. La documentación oficial de Microsoft sobre suscripción y retirada de eventos muestra esa retención en un mecanismo concreto.

El problema general es ownership: subscribe pero nunca unsubscribe puede producir objetos retenidos, recursos sin liberar y notificaciones inesperadas. Volver a crear la pantalla puede añadir una segunda suscripción y ejecutar dos veces una reacción.

Una registration handle o subscription token permite asociar la retirada con el cierre del propietario. Conviene definir si registrar dos veces el mismo interesado está permitido, si retirar es idempotente y cuándo una suscripción deja de ser válida.

Weak references pueden evitar ciertas retenciones, pero no sustituyen una política de lifecycle. Un interesado puede seguir recibiendo señales mientras exista, y una reacción pendiente puede conservar otras referencias. La necesidad semántica de dejar de reaccionar no se resuelve solamente esperando garbage collection.

Retirar tampoco implica que un callback ya iniciado o capturado en un snapshot se detenga. Si el consumidor requiere ausencia de nuevas actualizaciones tras su cierre, el contrato debe definir esa coordinación; puede necesitar comprobar que sigue activo antes de modificar una vista.

Ejecución síncrona, orden y failure

Observer no implica asynchronous execution. Una implementación puede ejecutar:

subject.notify()
    → observer A
    → observer B
    → observer C

síncronamente. Un observer lento demora el retorno del publisher. Una excepción puede detener el loop y dejar a los demás sin notificación, salvo que exista una política de aislamiento. Capturar todas las excepciones sin diagnóstico también puede esconder fallos de actualización.

El orden puede estar definido, ser incidental o no estar garantizado. Si B depende del trabajo de A, esa dependencia debe modelarse; confiar en un orden de registro accidental vuelve frágil la colaboración.

Reentrancy aparece cuando un observer modifica el Subject durante su callback y provoca otra notificación antes de finalizar la primera. Puede exponer estado intermedio, repetir trabajo o causar ciclos. Completar invariantes antes de notificar y definir cómo se agrupan o difieren cambios son posibles respuestas, según necesidad.

Un contrato explícito debe indicar quién ejecuta callbacks, cuándo se considera completa la publicación, qué errores llegan al emisor y si se admiten cambios del registro. Probar solo que notify llama a update no verifica esas obligaciones.

Variantes asíncronas y lenguaje

Agregar scheduler o queue puede desacoplar temporalmente publicación y ejecución. Introduce decisiones sobre buffer, thread o contexto, shutdown, orden, cancelación y tratamiento de fallos. Mover callbacks a otro thread no proporciona automáticamente almacenamiento durable ni coordinación distribuida.

Events, callbacks y reactive primitives pueden reducir ceremony y ofrecer mecanismos de suscripción. Hay que leer su contrato concreto: una biblioteca puede definir completion, errores o scheduling que Observer por sí solo no especifica. La documentación de Observable en Java incluso distingue las limitaciones de una API particular del problema más amplio de notificación.

Usar una feature de lenguaje no elimina lifecycle ni garantiza entrega. Un callback suministrado puede ser suficiente para un escenario pequeño; no hace falta escribir Subject y Observer como clases si la colaboración ya es clara.

Observer, Pub/Sub y eventos

Observer suele describir colaboración cercana entre componentes. Distributed Publish/Subscribe puede introducir broker, red, procesos independientes, durabilidad y políticas de consumo. El Publish-Subscribe Channel de Hohpe y Woolf pertenece al contexto de messaging.

Compartir vocabulario Publisher/Subscriber no vuelve equivalentes esos contratos. Consumer groups, replay, ordering y delivery guarantees dependen del sistema de mensajería; no nacen de un registro local de interesados. reliable-messaging y delivery-guarantees permiten examinar esas obligaciones sin atribuirlas a Observer.

Domain Event expresa un hecho significativo del dominio; Integration Event comunica entre fronteras de integración. Observer puede implementar reacciones locales a un evento, pero no determina su significado ni su contrato de publicación. Esos conceptos conservan sus ámbitos editoriales de Domain Modeling e Integration and Messaging.

Usar Observer tampoco demuestra Event-Driven Architecture. Esa decisión arquitectónica involucra comunicación, coordinación, temporal coupling, deployment, propiedad de eventos, consistencia e infraestructura. Una preview actualizada por callback no decide todas esas dimensiones.

Consecuencias, misuses y relaciones

Observer facilita extender interesados y retirar conocimiento de clases concretas del publisher. A cambio introduce flujo indirecto, suscripciones y políticas de orden, recursos y errores.

Es útil cuando varios dependientes legítimos reaccionan a cambios. Una llamada directa puede ser más clara para un único colaborador estable o una operación cuyo resultado necesita controlar estrictamente el emisor. No conviene usar notificaciones para esconder una secuencia obligatoria que debería ser explícita.

Event spaghetti aparece cuando señales encadenadas vuelven difícil rastrear causa y efecto. Suscripciones filtradas, orden desconocido, callbacks lentos, reentrancy y cascadas sin límites son misuses de la colaboración, no razones para rechazarla siempre.

mediator-pattern coordina interacciones entre colaboradores; Observer distribuye notificaciones a interesados. Un Mediator puede recibir eventos y decidir actualizaciones, combinando ambos patrones. La notificación no determina por sí sola quién gobierna una secuencia obligatoria.

presentation-patterns contextualiza sincronización de UI. strategy-pattern selecciona comportamiento, mientras Observer distribuye notificaciones. pattern-selection permite comparar esas intenciones y costos; design-patterns vuelve a las familias sin equiparar mecanismo local y arquitectura.