Programación · Patrones de diseño
Patrón Observer explicado con ejemplos
Respuesta corta
Observer es un patrón de diseño de comportamiento en el que un objeto, llamado sujeto, mantiene una lista de dependientes, llamados observadores, y los notifica automáticamente cuando cambia su estado. Los observadores se suscriben y se dan de baja en tiempo de ejecución, y el sujeto no necesita conocer sus clases concretas. Los listeners de eventos del navegador, el EventEmitter de Node.js y las bibliotecas reactivas como RxJS se basan en esta idea.
Intención y el problema que resuelve
Intención: definir una dependencia de uno a muchos entre objetos, de modo que cuando un objeto cambia de estado, todos sus dependientes sean notificados y se actualicen automáticamente. Observer es uno de los patrones de comportamiento del libro de la Banda de los Cuatro (Gang of Four, GoF), y también se conoce como publicación-suscripción o patrón listener.
Imagina un carrito de compras en una tienda web. Cuando se añade un artículo, el icono del carrito en la cabecera debe mostrar el nuevo recuento, la barra lateral debe mostrar el nuevo total y un módulo de analítica debe registrar el evento. Si el carrito llama directamente a cada uno, el módulo del carrito depende de la cabecera, la barra lateral y la analítica. Añadir una cuarta reacción significa editar de nuevo el carrito.
Con Observer, el carrito solo mantiene una lista de suscriptores y llama a cada uno cuando cambia. La cabecera, la barra lateral y la analítica se suscriben por sí mismas. El carrito no las importa, y nuevas funciones pueden reaccionar a los cambios del carrito sin tocar su código.
Estructura y participantes
| Rol | Responsabilidad |
|---|---|
| Sujeto (publicador) | Mantiene una lista de observadores y ofrece métodos para suscribirlos y darlos de baja; los notifica cuando algo cambia |
| Observador (suscriptor) | Interfaz con un método de actualización, a menudo solo una función callback |
| Sujeto concreto | Guarda el estado de interés y llama a notify cuando cambia |
| Observador concreto | Reacciona a las notificaciones, por ejemplo redibujando una parte de la interfaz |
Hay dos estilos de notificación. En el modelo push, el sujeto envía los datos cambiados junto con la notificación. En el modelo pull, solo dice "algo cambió" y los observadores piden al sujeto los datos que necesitan.
Implementación en TypeScript
Una pequeña clase genérica Subject gestiona las suscripciones. Los observadores son funciones simples, que es la forma más común en TypeScript. subscribe devuelve una función que cancela la suscripción, así que quien llama no puede olvidar qué listener registró.
type Listener<T> = (value: T) => void;
class Subject<T> {
private listeners = new Set<Listener<T>>();
subscribe(listener: Listener<T>): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
protected notify(value: T) {
for (const listener of [...this.listeners]) listener(value);
}
}
type CartEvent = { count: number; total: number };
class Cart extends Subject<CartEvent> {
private items: { name: string; price: number }[] = [];
add(name: string, price: number) {
this.items.push({ name, price });
const total = this.items.reduce((sum, item) => sum + item.price, 0);
this.notify({ count: this.items.length, total });
}
}
const cart = new Cart();
cart.subscribe(({ count }) => console.log("header: " + count + " items"));
const stopAnalytics = cart.subscribe(({ total }) => console.log("analytics: total " + total));
cart.add("Book", 12);
stopAnalytics(); // unsubscribe
cart.add("Pen", 3);
// header: 1 items
// analytics: total 12
// header: 2 itemsnotify recorre una copia del conjunto ([...this.listeners]). De este modo, un listener que se da de baja a sí mismo durante la notificación no altera el bucle.
Usar el EventTarget integrado
Los navegadores, Node.js y Deno ofrecen EventTarget, así que a menudo no necesitas tu propia clase de sujeto:
const thermostat = new EventTarget();
thermostat.addEventListener("change", (e) => {
console.log("new temperature:", (e as CustomEvent<number>).detail);
});
thermostat.dispatchEvent(new CustomEvent("change", { detail: 21.5 }));
// new temperature: 21.5Node.js también tiene la clase EventEmitter en el módulo node:events, con los métodos on, off y emit.
Cuándo usarlo y cuándo no
- Úsalo cuando un cambio en un objeto deba actualizar a otros objetos y no sepas de antemano cuántos ni cuáles.
- Úsalo para mantener un módulo central independiente del código de interfaz, registro o analítica que reacciona a él.
- Úsalo para eventos que ocurren a lo largo del tiempo, como entradas del usuario, mensajes o lecturas de sensores.
- No lo uses cuando hay exactamente un receptor fijo; una llamada directa a un método o un parámetro callback es más fácil de seguir.
- Ten cuidado con las cadenas de observadores que disparan otras notificaciones. Pueden causar bucles de actualización y hacer difícil ver por qué algo cambió.
- No dependas del orden en que se llama a los observadores a menos que el sujeto lo documente.
Usos en el mundo real
- El DOM:
element.addEventListener("click", handler)suscribe un observador a un elemento. - Node.js:
EventEmitteres la base de los flujos, los servidores HTTP y muchos otros objetos del núcleo. - Las API de observadores del navegador como
MutationObserver,IntersectionObserveryResizeObservernotifican a callbacks los cambios en la página. - RxJS modela flujos de valores como observables a los que se suscriben los observadores.
- Java:
java.util.Observablequedó obsoleta en Java 9; en su lugar se usan las interfacesjava.util.concurrent.Flow(publicador y suscriptor) y las interfaces de listeners de eventos.
Patrones relacionados
| Patrón | En qué se diferencia de Observer |
|---|---|
| Mediator | Un mediador conoce a los objetos participantes y los coordina; un sujeto simplemente difunde y no sabe qué hacen los observadores. |
| Publicación-suscripción con un broker | Los publicadores y los suscriptores no se conocen en absoluto y se comunican a través de un broker de mensajes o un bus de eventos, a menudo entre procesos. |
| Command | Una notificación puede llevar un objeto comando; Command trata de una petición, Observer de una notificación de uno a muchos. |
| Iterator | Iterator es de tipo pull (el consumidor pide el siguiente valor); Observer es de tipo push (el productor envía los valores cuando ocurren). |
Preguntas frecuentes
¿Cuál es la diferencia entre Observer y publicación-suscripción?
En el patrón Observer clásico, los observadores se registran directamente en el sujeto. En publicación-suscripción, un broker o canal de eventos se sitúa entre ellos, así que publicadores y suscriptores no se conocen. Mucha gente usa los dos nombres de forma laxa para la misma idea.
¿Cómo evito fugas de memoria con el patrón Observer?
Da de baja a los observadores cuando ya no se necesiten. Devolver una función de baja desde subscribe, o usar un AbortSignal con addEventListener, simplifica la limpieza.
¿Se llama a los observadores de forma síncrona?
En el ejemplo anterior y en EventTarget y EventEmitter, los listeners se ejecutan de forma síncrona en el orden en que se añadieron. Otras bibliotecas pueden entregar las notificaciones de forma asíncrona, así que consulta su documentación.
¿Observer es lo mismo que la programación reactiva?
La programación reactiva se construye sobre Observer. Bibliotecas como RxJS añaden operadores para filtrar, combinar y transformar flujos de notificaciones, además de reglas para errores y finalización.