19. Observer (Observador) - Patrón de Comportamiento

El patrón Observer define una dependencia uno-a-muchos entre objetos, de modo que cuando uno cambia de estado todos sus observadores son notificados y actualizados automáticamente. Se basa en una suscripción desacoplada que permite reaccionar a eventos sin que el emisor conozca detalles de los receptores.

Es la piedra angular de arquitecturas reactivas, buses de eventos y interfaces gráficas: separa la emisión de cambios de su consumo, fomenta la extensibilidad y habilita componer comportamientos observando las mismas fuentes de datos.

19.1 Problema y contexto

En sistemas interactivos es habitual que varios componentes necesiten reaccionar ante un mismo cambio: actualizar dashboards cuando llega un pedido, recalcular stock, enviar notificaciones y registrar auditorías. Implementarlo sin Observer conduce a acoplamientos fuertes donde el sujeto debe invocar explícitamente a cada interesado, lo que genera dependencias cíclicas y dificulta agregar nuevos suscriptores.

Observer introduce un mecanismo de publicación-suscripción in-process: el sujeto sólo conoce una interfaz genérica de observador y notifica a todos los suscritos cuando ocurre un evento, evitando condicionales y referencias directas.

19.2 Intención y motivación

La intención es definir un protocolo para que objetos interesados se suscriban a cambios en otro objeto sin acoplamiento fuerte. Su aplicación es pertinente cuando:

  • Se requiere propagar eventos a múltiples componentes con responsabilidades distintas.
  • La lista de suscriptores debe variar dinámicamente durante la ejecución.
  • Se busca mantener abierto el sistema a nuevas reacciones sin modificar el emisor.
  • Se necesitan difundir cambios a distintas capas (persistencia, interfaz, monitoreo) de forma independizable.

El patrón motiva a separar la publicación de eventos de la lógica de consumo y a mantener un contrato estable que permita crecer en funcionalidades sin tocar el sujeto.

19.3 Participantes y estructura

Observer cuenta con los siguientes participantes:

  • Subject: entidad que mantiene el estado observado, permite suscribir o quitar observadores y lanza notificaciones.
  • Observer: interfaz que define la operación invocada cuando ocurre un evento.
  • ConcreteSubject: implementación que almacena los observadores y decide cuándo notificar.
  • ConcreteObserver: componentes que reaccionan al evento según sus propias responsabilidades.
  • Cliente: configura las suscripciones y dispara las operaciones del sujeto.

La notificación puede ser sincrónica (el sujeto llama secuencialmente a cada observador) o asincrónica delegando en hilos, colas o reactores según las necesidades de escalabilidad.

19.4 Sincronía, asincronía y garantías de entrega

Elegir cómo se distribuyen los eventos es crucial:

  • Sincrónico: el sujeto procesa a los observadores en el mismo hilo. Es simple y garantiza orden, pero un observador lento puede bloquear al resto.
  • Asincrónico: se delega a ejecutores o colas. Mejora la latencia y desacopla tiempos, aunque agrega complejidad y requiere definir políticas de reintento y orden.

Las garantías de entrega (al menos una, exactamente una, como máximo una) dependen de la estrategia seleccionada y de la idempotencia de los observadores. Observer proporciona la base, pero la confiabilidad debe diseñarse caso a caso.

19.5 Escenario: seguimiento de pedidos en un marketplace

Consideremos un marketplace que desea notificar en tiempo real cuando un pedido cambia de estado. Distintas áreas necesitan reaccionar: operaciones logísticas actualiza rutas, marketing envía mensajes y compliance registra auditorías. Las suscripciones deben poder activarse o desactivarse sin interrumpir el sistema.

Observer permite que el centro de eventos de pedidos publique cada cambio y que las áreas interesadas se suscriban, agregando nuevos observadores según las innovaciones del negocio sin modificar el código del sujeto.

19.6 Implementación en C#

El siguiente código muestra una implementación con suscriptores independientes y protección frente a condiciones de carrera mediante una sección crítica controlada por lock:

using System;
using System.Collections.Generic;

namespace Tutorial.Observer
{
    public enum EstadoPedido
    {
        Creado,
        Preparado,
        Enviado,
        Entregado,
        Cancelado
    }

    public sealed class EventoCambioEstado
    {
        public EventoCambioEstado(string idPedido, EstadoPedido estado, DateTimeOffset fecha, string origen)
        {
            IdPedido = idPedido ?? throw new ArgumentNullException(nameof(idPedido));
            Estado = estado;
            Fecha = fecha;
            Origen = origen ?? throw new ArgumentNullException(nameof(origen));
        }

        public string IdPedido { get; }
        public EstadoPedido Estado { get; }
        public DateTimeOffset Fecha { get; }
        public string Origen { get; }
    }

    public interface IObservadorCambioEstado
    {
        void Notificar(EventoCambioEstado evento);
    }

    public interface ICentroEventosPedido
    {
        void Suscribir(IObservadorCambioEstado observador);
        void Cancelar(IObservadorCambioEstado observador);
        void Publicar(EventoCambioEstado evento);
    }

    public sealed class CentroEventosPedido : ICentroEventosPedido
    {
        private readonly List<IObservadorCambioEstado> _observadores = new List<IObservadorCambioEstado>();
        private readonly object _candado = new object();

        public void Suscribir(IObservadorCambioEstado observador)
        {
            if (observador == null) throw new ArgumentNullException(nameof(observador));

            lock (_candado)
            {
                if (!_observadores.Contains(observador))
                {
                    _observadores.Add(observador);
                }
            }
        }

        public void Cancelar(IObservadorCambioEstado observador)
        {
            if (observador == null) return;

            lock (_candado)
            {
                _observadores.Remove(observador);
            }
        }

        public void Publicar(EventoCambioEstado evento)
        {
            if (evento == null) throw new ArgumentNullException(nameof(evento));

            List<IObservadorCambioEstado> copia;

            lock (_candado)
            {
                copia = new List<IObservadorCambioEstado>(_observadores);
            }

            foreach (var observador in copia)
            {
                observador.Notificar(evento);
            }
        }
    }

    public sealed class ObservadorLogistica : IObservadorCambioEstado
    {
        public void Notificar(EventoCambioEstado evento)
        {
            Console.WriteLine($"[Logistica] Pedido {evento.IdPedido} => {evento.Estado} (origen: {evento.Origen})");
        }
    }

    public sealed class ObservadorMarketing : IObservadorCambioEstado
    {
        public void Notificar(EventoCambioEstado evento)
        {
            Console.WriteLine($"[Marketing] Preparar campana para {evento.IdPedido} en estado {evento.Estado}");
        }
    }

    public sealed class ObservadorAuditoria : IObservadorCambioEstado
    {
        public void Notificar(EventoCambioEstado evento)
        {
            Console.WriteLine($"[Auditoria] Registro de {evento.IdPedido} a las {evento.Fecha:O}");
        }
    }

    public sealed class ServicioPedidos
    {
        private readonly ICentroEventosPedido _centro;

        public ServicioPedidos(ICentroEventosPedido centro)
        {
            _centro = centro ?? throw new ArgumentNullException(nameof(centro));
        }

        public void CambiarEstado(string idPedido, EstadoPedido estado, string origen)
        {
            var evento = new EventoCambioEstado(idPedido, estado, DateTimeOffset.UtcNow, origen);
            _centro.Publicar(evento);
        }
    }

    public static class AplicacionObserver
    {
        public static void Main()
        {
            ICentroEventosPedido centroEventos = new CentroEventosPedido();
            centroEventos.Suscribir(new ObservadorLogistica());
            centroEventos.Suscribir(new ObservadorMarketing());
            centroEventos.Suscribir(new ObservadorAuditoria());

            var servicio = new ServicioPedidos(centroEventos);
            servicio.CambiarEstado("PED-1001", EstadoPedido.Preparado, "backend");
            servicio.CambiarEstado("PED-1001", EstadoPedido.Enviado, "sistema-rastreos");
            servicio.CambiarEstado("PED-1001", EstadoPedido.Entregado, "app-repartidores");
        }
    }
}

19.7 Explicación del flujo

ServicioPedidos actúa como sujeto y delega la notificación en CentroEventosPedido, que mantiene una lista segura para acceso concurrente. Cada observador aplica sus propias reglas sin conocer a los demás. Agregar un nuevo suscriptor o remover uno existente no requiere modificar el servicio.

El uso de un lock centralizado asegura que la lista de observadores se mantenga consistente aun cuando múltiples hilos intenten suscribirse o cancelar al mismo tiempo. Si la cantidad de eventos creciera, podría reemplazarse por estructuras concurrentes o por colas asíncronas administradas por Task.

19.8 Observer en el ecosistema .NET

La biblioteca base de .NET incluye IObservable<T> e IObserver<T> para modelar flujos de eventos desacoplados, junto con utilidades como PropertyChangedEventHandler para notificar cambios de propiedades. Frameworks como WPF, WinUI, Blazor o ASP.NET Core se apoyan en estos contratos y, cuando se necesita mayor sofisticación, Reactive Extensions (Rx.NET) permite componer consultas sobre secuencias, aplicar retro-presión y orquestar tareas asíncronas.

19.9 Variantes y extensiones

Observer puede combinarse con patrones de mensajes cuando la escala crece. Las variantes incluyen:

  • Push: el sujeto envía directamente los datos necesarios.
  • Pull: el observador recibe una señal y consulta los datos que necesita.
  • Observadores filtrados: el sujeto clasifica eventos y notifica solo a quienes declararon interés en ciertos tipos.

En escenarios reactivamente complejos surgen soluciones como Reactive Streams, que formalizan este patrón incorporando presión inversa y cancelaciones.

19.10 Riesgos y malas prácticas

Un riesgo común es olvidar cancelar suscripciones: los observadores pueden seguir recibiendo notificaciones innecesarias o provocar fugas de memoria. Otro problema es notificar sin protección ante errores; si un observador lanza una excepción sin control, el resto de suscriptores puede quedar sin aviso. También se debe evitar que los observadores modifiquen el sujeto de forma directa, para impedir actualizaciones recursivas.

19.11 Buenas prácticas para aplicar Observer

  • Definir interfaces específicas que comuniquen claramente el tipo de evento difundido.
  • Registrar y cancelar suscripciones dentro de bloques bien delimitados del ciclo de vida del observador.
  • Registrar logs y métricas de las notificaciones para detectar retrasos o errores.
  • Evaluar el uso de colas o ejecución asíncrona cuando las tareas derivadas sean costosas.

19.12 Relación con otros patrones

Observer se complementa con Mediator cuando se necesita coordinar la comunicación entre observadores. Puede convivir con Command para encapsular las acciones disparadas por cada notificación y con Chain of Responsibility cuando los eventos atraviesan filtros secuenciales. Además, es un fundamento clave para la infraestructura de eventos utilizada por Strategy y State para reaccionar ante cambios externos.