El patrón Observador en C#



Quizá este sea el patrón más utilizado en programación, no solo en C#, sino en la mayoría de lenguajes.

La implementación en C# es muy sencilla y cualquiera que haya realizado una aplicación en WebForms, WinForms o WPF habrá aplicado este patrón aunque sea sin haberse dado cuenta. ¿Cómo puedo estar tan seguro? Porque la implementación en C# de este patrón son los eventos y dudo que haya programadores que no los utilicen :-)

En este post voy a explicar los conceptos y las ideas que hay detrás del patrón, mostraré diferentes versiones del patrón y finalmente implementaré un ejemplo utilizando los eventos, que es como se debería utilizar C#.

Avísame cuando haya alguna novedad

Pues eso, cuando haya algún cambio me envías un whatsapp o un e-mail o me llamas directamente.

Esa es la idea principal del patrón, enviar notificaciones a todos aquellos que estén interesados en saber sobre un evento.

El sujeto o publicador es el objeto que envía las notificaciones. Para ello mantiene una colección interna de subscriptores u observadores, y cuando sucede un evento en concreto, el publicador recorre todos los subscriptores de su colección y les envía la información.

Para que todo esto sea posible el patrón define dos interfaces:

IPublicador:

   public interface IPublicador
   {
       void RegistrarObservador(IObservador observador);
       void QuitarObservador(IObservador observador);

       void Notificar();
   }

IObservador

 public  interface IObservador
 {
      void Actualizar();
 }

El observador sólo necesita un método, Actualizar(). A través de él el publicador le indica que algo ha sucedido.

Por su parte el publicador necesita un método para notificar a los subscriptores, Notificar() y otros dos para manipular su colección interna de subscriptores: el método de RegistrarSubscriptor() añadirá un nuevo subscriptor a la colección, mientras que el método EliminarSubscripción() quitará el subscriptor de la colección.

El ejemplo del Tiempo

Resulta que hay una clase que es capaz de obtener la temperatura, la humedad y la presión del tiempo. Como una estación meteorológica.

Además tiene un método que se dispara cada vez que una de esas tres medidas cambia.

   public class EstacionMetereologica
    {
        public decimal Temperatura { get;  }
        public decimal Presion { get;  }
        public decimal Humedad { get; }

        public void HaCambiadoAlgunaMedida()
        {
            // código
        }

        // Otros métodos
    }

Por otro lado, tenemos tres tipos de dispositivos diferentes, cuya misión es mostrar datos de temperatura, presión y humedad:

  • DispositivoTiempoActual: muestra la temperatura, presión y humedad actual.
  • DispositivoEstadisticas: muestra las estadísticas de temperatura, presión y humedad.
  • DispositivoPredictivo: muestra una predicción de temperatura, presión y humedad.

¿Cómo podríamos programar los dispositivos para que cada vez que haya un cambio de temperatura se actualicen?

Veamos una primera versión de la clase EstacionMeteorologica:

public class EstacionMetereologicaVersion1
    {
        public decimal Temperatura { get;  }
        public decimal Presion { get;  }
        public decimal Humedad { get;  }


        private readonly DispositivoTiempoActual _dispositivoTiempoActual;
        private readonly DispositivoEstadisticas _dispositivoEstadisticas;
        private readonly DispositivoPredictivo _dispositivoPredictivo;
        // ... siguientes dispositivos
        
        public EstacionMetereologicaVersion1(
                DispositivoTiempoActual dispositivoTiempoActual, 
                DispositivoEstadisticas dispositivoEstadisticas, 
                DispositivoPredictivo dispositivoPredictivo
                // ... siguientes dispositivos
            )
        {
            _dispositivoTiempoActual = dispositivoTiempoActual;
            _dispositivoEstadisticas = dispositivoEstadisticas;
            _dispositivoPredictivo = dispositivoPredictivo;
            // ... siguientes dispositivos
        }

        public void HaCambiadoElTiempo()
        {
            _dispositivoTiempoActual.Actualizar(Temperatura, Presion, Humedad);
            _dispositivoEstadisticas.Actualizar(Temperatura, Presion, Humedad);
            _dispositivoPredictivo.Actualizar(Temperatura, Presion, Humedad);
            // ... siguientes dispositivos
        }
    }

El principal problema de este diseño es que la clase no queda cerrada ante posibles cambios. Si por ejemplo aparece un nuevo dispositivo, habrá que “abrir” la clase para añadir el dispositivo. Hay cuatro puntos de la clase donde habría que añadir líneas de código. Los he marcado con el comentario 

"// ... siguientes dispositivos

Si hay pocos dispositivos sería pasable. Me refiero a dos o tres dispositivos, si hay más ya serían multitud.

Aplicando el patrón Observador

Si definimos la estación meteorológica como un publicador y los dispositivos como observadores podríamos aplicar el patrón de la siguiente manera:

  public class EstacionMetereologica : IPublicador
    {
        public decimal Temperatura { get; }
        public decimal Presion { get; }
        public decimal Humedad { get; }

        private readonly List<IObservador> _observadores; 
        
        public EstacionMetereologicaVersion2(List<IObservador> observadores)
        {
            _observadores = observadores;
         }

        public void HaCambiadoElTiempo()
        {
           Notificar();
        }

        public void Notificar()
        {
            foreach (var observador in _observadores)
            {
                observador.Actualizar(Temperatura, Presion, Humedad);
            }
        }

        public void RegistrarObservador(IObservador observador)
        {
            _observadores.Add(observador);
        }

        public void QuitarObservador(IObservador observador)
        {
            _observadores.Remove(observador);
        }
    }

La clave es el método notificar, que recorre todos los observadores y llama al método Actualizar() en cada uno de ellos.

Con este diseño podríamos ir creando tantos dispositivos como quisiéramos sin la necesidad de modificar la clase. Es decir, la clase ha quedado cerrada.

Definición del patrón Observador

Define una dependencia del tipo "uno a muchos" entre objetos, de manera que cuando uno de los objetos cambia su estado, notifica este cambio a todos los dependientes.

Participantes

  • Sujeto o publicador (en el ejemplo la IPublicador)
    • Conoce a sus observadores. Puede tener tantos Observadores como desee.
    • Provee una interfaz que permite Adjuntar o Quitar Observadores.
       
  • Observador (en el ejemplo IObservador) 
    • Define una interfaz para actualizar los objetos que deben ser notificados ante cambios en un sujeto.
       
  • Sujeto o publicador concreto (EstacionMeteorologica)
    • Almacena un estado de interés para los Observadores.
       
  • Observador concreto (en el ejemplo un dispositivo)
    • Mantiene una referencia del Sujeto o Publicador concreto.
    • Guarda un estado que debería ser consistente con el Sujeto.
    • Implementa la interfaz de actualización del Observador para mantener su estado consistente con el Sujeto.

¿Me llamas o te llamo?

Hay dos maneras diferentes de pasar el estado del publicador al subscriptor: “push” (me llamas)  y “pull” (mejor te llamo yo).

En el ejemplo, hemos visto que en la llamada observador.Actualizar(Temperatura, Presion, Humedad) el estado representado por las medidas se pasa directamente a través de parámetros. Es el caso "me llamas" porque el observador está esperando recibir el estado directamente. No necesita saber nada del publicador y por tanto no necesita una referencia a él.

Por otro lado, si nos ceñimos estrictamente a la definición y en lugar de pasar los parámetros no pasamos nada, lo que realmente se está haciendo es indicar al observador que ha sucedido un cambio. ¿Cuál? Que llame el observador y pregunte:

  public class DispositivoTiempoActual : IObservador
    {
        private readonly EstacionMetereologica _estacionMetereologica;
        
        public DispositivoTiempoActual(EstacionMetereologica estacionMetereologica)
        {
            _estacionMetereologica = estacionMetereologica;
        }

        public void Actualizar()
        {
            // ... código

            // Cuando me vaya bien, llamo al publicador para conocer las medidas
            var temperatura = _estacionMetereologica.Temperatura;
            var presion = _estacionMetereologica.Presion;
            var humedad = _estacionMetereologica.Humedad;

            // ... código
        }
    }

En primer lugar se inyecta el publicador a través del constructor. Así el dispositivo mantiene una referencia a la estación meteorológica.

En el momento que se llama a Actualizar() el observador puede utilizar la referencia del publicador (estación meteorológica) para conocer el estado (las medidas).

En C# el patrón observador se implementa con eventos

Un inconveniente a los ejemplos que hemos visto hasta ahora es que el publicador tiene que conocer a los observadores, y al revés, los observadores tienen que saber del publicador (cuando se trata de "pull").

Pero si se utilizan eventos para implementar el patrón, dichas dependencias desaparecen. Ya no hay que crear interfaces y las clases quedan completamente desacopladas. Cualquiera puede ser un Publicador y cualquiera puede ser un Observador.

    public class EstacionMetereologica
    {
        public decimal Temperatura { get; private set; }
        public decimal Presion { get; private set; }
        public decimal Humedad { get; private set; }

        public event EventHandler<Tuple<decimal, decimal, decimal>> HaCambiadoElTiempo;

        public void AumentarLaTemperaturaEnGrados(int grados)
        {
            Temperatura = grados + 1;

            Notificar();
        }

        public void Notificar()
        {
            var medidas = new Tuple<decimal, decimal, decimal>(Temperatura, Humedad, Presion);

            if (HaCambiadoElTiempo != null)
                HaCambiadoElTiempo.Invoke(this, medidas);
        }
    }

La clase se ha simplificado enormemente. Por un lado, no debe implementar ninguna interfaz IPublicador, y por el otro no es necesario tener una colección interna de observadores. Además ahorra los métodos de RegistrarObservador y EliminarObservador. Todo son ventajas :)

Las medidas las he encapsulado en una Tupla para que puedan pasar como un solo parámetro, pero también podría haberlas encapsulado en una clase. 

¿Cómo subscribirse al evento HaCambiadoElTiempo?

    class Program
    {
        static void Main(string[] args)
        {
            EstacionMetereologica estacionMetereologica = new EstacionMetereologica();

            DispositivoTiempoActual dispositivoTiempoActual = new DispositivoTiempoActual();
            DispositivoEstadisticas dispositivoEstadisticas = new DispositivoEstadisticas();
            DispositivoPredictivo dispositivoPredictivo = new DispositivoPredictivo();
            
            estacionMetereologica.HaCambiadoElTiempo += dispositivoTiempoActual.ActualizarPantallaDipositivo;
            estacionMetereologica.HaCambiadoElTiempo += dispositivoEstadisticas.AñadirDatosParaLasEstadisticas;
            estacionMetereologica.HaCambiadoElTiempo += dispositivoPredictivo.AñadirDatosDePrediccion;

            estacionMetereologica.AumentarLaTemperaturaEnGrados(1);
            
            Console.ReadLine();

        }
    }

Cualquier dispositivo se puede suscribir al evento si declara una función que cumpla la firma de un delegado:

    public class DispositivoTiempoActual
    {
       // otras métodos y propiedades del dispositivo

        public void ActualizarPantallaDipositivo(object sender, Tuple<decimal, decimal, decimal> medidas)
        {
            var temperatura = medidas.Item1;
            var presion = medidas.Item2;
            var humedad = medidas.Item3;

            // cosas importantes que hacer con las medidas
        }
    }

Con esta nueva manera de implementar el patrón los observadores tampoco están ligados a cumplir con ninguna interfaz.

Conclusiones

  • El patrón observador es posiblemente el más utilizado en programación. Casi todos los lenguajes o frameworks tienen una implementación del patrón.
     
  • Principalmente sirve para enviar notificaciones cuando ha cambiado el estado de un objeto.
     
  • La mejor práctica en C# para implementar el patrón es utilizar los eventos.

Referencias

El ejemplo del tiempo está sacado de Head First Dessign Patterns




Quizá algun día empiece a enviar una newsletter, si te gustaría recibirla subscríbete aquí

Archivo