Saltar al contenidoJMRG
Todas las entradas

Patrón Observer: Reaccionando a Cambios de Estado

design-patternsbehavioralobserver

¿Qué es el Patrón Observer?

El patrón Observer define una dependencia uno-a-muchos entre objetos de manera que cuando un objeto cambia de estado, todos sus dependientes son notificados y actualizados automáticamente. Es la base de los sistemas reactivos y event-driven que usamos a diario.

classDiagram
    class Subject {
        -observers: Observer[]
        +attach(observer)
        +detach(observer)
        +notify()
    }

    class Observer {
        <<interface>>
        +update(state)
    }

    class ConcreteObserver {
        -state
        +update(state)
    }

    Subject "1" --> "*" Observer : notifica
    Observer <|.. ConcreteObserver

Si has trabajado con React, Vue, o simplemente con eventos del DOM, ya has usado este patrón sin saberlo.

El Problema que Resuelve

Imagina que tienes un objeto "fuente de verdad" (como el estado de un carrito de compras) y múltiples componentes que necesitan reaccionar cuando ese estado cambia:

  • El contador de items en el header
  • La lista de productos en el carrito
  • El total a pagar
  • Un indicador de envío gratis

Sin el patrón Observer, tendrías que actualizar manualmente cada componente cada vez que el carrito cambie.

Implementación Paso a Paso

1. Event Emitter Tipado

type EventMap = Record<string, unknown>;
type EventCallback<T> = (data: T) => void;
/**
 * Emisor de eventos genérico con tipos seguros
 * @template Events - Mapa de eventos y sus tipos de datos
 */
class TypedEventEmitter<Events extends EventMap> {
  private listeners = new Map<keyof Events, Set<EventCallback<any>>>();
  /** Suscribe un callback a un evento, retorna función para desuscribir */
  on<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): () => void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(callback);
    return () => this.off(event, callback);
  }
  off<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
    this.listeners.get(event)?.delete(callback);
  }
  emit<K extends keyof Events>(event: K, data: Events[K]): void {
    this.listeners.get(event)?.forEach(callback => callback(data));
  }
}

2. Tipos de Eventos

interface StoreEvents<T> {
  change: { previous: T; current: T };
  error: Error;
}

3. Store Reactivo

/**
 * Store reactivo con suscripciones selectivas
 * @template T - Tipo del estado
 */
class ReactiveStore<T extends object> extends TypedEventEmitter<StoreEvents<T>> {
  private state: T;
  constructor(initialState: T) {
    super();
    this.state = structuredClone(initialState);
  }
  getState(): Readonly<T> {
    return structuredClone(this.state);
  }
  update(updater: (state: T) => void): void {
    const previous = structuredClone(this.state);
    updater(this.state);
    this.emit('change', { previous, current: structuredClone(this.state) });
  }
  /** Suscribe solo a cambios en una parte específica del estado */
  subscribe(selector: (state: T) => unknown, callback: (state: T) => void): () => void {
    let previousValue = selector(this.state);
    return this.on('change', ({ current }) => {
      const currentValue = selector(current);
      if (previousValue !== currentValue) {
        previousValue = currentValue;
        callback(current);
      }
    });
  }
}

Ejemplo Práctico: Carrito de Compras

interface AppState {
  user: { name: string; email: string } | null;
  cart: { items: Array<{ id: string; quantity: number; price: number }> };
  ui: { theme: 'light' | 'dark'; loading: boolean };
}
const store = new ReactiveStore<AppState>({
  user: null,
  cart: { items: [] },
  ui: { theme: 'light', loading: false },
});
const unsubTheme = store.subscribe(
  state => state.ui.theme,
  state => console.log('Theme changed to:', state.ui.theme)
);
const unsubCart = store.subscribe(
  state => state.cart.items.length,
  state => console.log('Cart updated:', state.cart.items.length, 'items')
);
store.update(state => { state.ui.theme = 'dark'; });
store.update(state => { state.cart.items.push({ id: 'item1', quantity: 2, price: 29.99 }); });
unsubTheme();
unsubCart();

Cuándo Usar Observer

Situación Recomendación
Sistema de eventos tipado Uso recomendado
Store reactivo (estado global) Uso recomendado
Notificaciones en tiempo real Uso recomendado
Comunicación directa simple Considerar alternativas

Cuándo NO Usar Observer

  • Comunicación uno-a-uno: Si solo hay un receptor, una llamada directa es más simple
  • Orden de ejecución crítico: El orden de notificación no está garantizado
  • Performance crítica: Cada cambio notifica a todos los observadores

Errores Comunes

  1. Memory leaks: No olvidar desuscribirse cuando el componente se destruye
  2. Actualizaciones en cascada: Un observer que modifica el estado puede causar loops infinitos
  3. Over-notification: Notificar cambios que no son relevantes para la mayoría de observers

Conclusión

El patrón Observer es fundamental para construir sistemas reactivos y desacoplados. Es la base de librerías como RxJS, Redux, MobX, y el sistema de reactividad de Vue.

En el próximo artículo exploraremos el Patrón Strategy, que nos permite intercambiar algoritmos en tiempo de ejecución.


Basado en "Design Patterns: Elements of Reusable Object-Oriented Software" (Gang of Four).