Patrones de Comportamiento (VI): Patrón Observer


Objetivo:

«Definir una dependencia uno-a-muchos entre objetos de forma de que, cuando el estado de uno de ellos cambia, todos los objetos dependientes son notificados y actualizados de forma automática».

Design Patterns: Elements of Reusable Object-Oriented Software

Podemos afirmar sin rubor alguno que el patrón Observer es uno de los más importantes (y utilizados) de todos los patrones de diseño vistos hasta el momento. Su filosofía es simple: un objeto, denominado sujeto (Subject) posee un estado. Cuando su estado cambia, es capaz de «avisar» a sus subcriptores (Observers) de este cambio de estado. De este modo, los objetos suscritos al objeto no tienen que preocuparse de cuándo se produce un cambio de estado: éste se encargará de informar de forma activa a todos aquellos objetos que hayan decidido suscribirse.

Este tipo de suscripción puede ser de dos tipos:

  • Suscripción push: el objeto informa a sus suscriptores con sus valores tan pronto como su estado cambie.
  • Suscripción pull: el objeto es interrogado por sus suscriptores si su estado ha cambiado desde la última vez que se tanteó.

Este esquema respeta al máximo el principio de bajo acoplamiento. El Subject y el Observer pueden interactuar entre ellos, pero apenas tienen conocimiento del uno sobre el otro. Dado que hemos basado el diseño en abstracciones, no en concreciones, no será necesario modificar el Subject para añadir nuevos Observers (bastará con que implemente la interfaz IObserver). Del mismo modo, un Observer podrá suscribirse a más de un Subject si éste implementa la interfaz ISubject.

Vale, hemos entendido el concepto pero ¿cómo funciona realmente el patrón? Explicado de forma simple, podemos resumirlo de la siguiente manera:

  • SubjectConcreto implementa la interfaz ISubject. Esta clase tendrá una funcionalidad propia, un estado (por ejemplo, una serie de variables) y una lista interna de objetos que implementan la interfaz IObserver. Esta lista se utilizará para realizar la «suscripción» a los cambios de estado, invocando el método IObserver.update() de cada objeto dentro de esta lista cada vez que se produzca un cambio de estado. Además, esta clase implementará los métodos de ISubject:
    • registrarObserver(IObserver o): añade el observer «o» pasado como parámetro a la lista de objetos que serán notificados.
    • eliminarObserver(Observer o): elimina el observer «o» pasado como parámetro de la lista de objetos que serán notificados.
    • notificarObservers(): recorre la lista de observers, invocando el método update() de cada uno de ellos, provocando que se realice una acción determinada cuando el estado del Subject cambie. Normalmente este método será invocado en el momento en el que el estado del Subject cambie (por ejemplo, al final del método setState()).

El método update() será implementado por cada Observer concreto, y se encargará de realizar las operaciones que el objeto suscrito quiera realizar cuando se entere de que algo ha cambiado dentro del Subject.

Implementando el patrón Observer

Realizaremos una implementación de este patrón basándonos en un sistema de sensores que realice una medición de los niveles de agua y aceite, así como de la presión de los neumáticos. Este sistema permitirá que los observers puedan suscribirse a los cambios en los valores de estos niveles, realizando las operaciones que estimen oportunas. Comenzaremos creando la interfaz IObserver que incluirá el método update que tendrán que implementar todos aquellas clases que deseen recibir notificaciones. Este método recibirá un parámetro que contendrá información sobre la actualización. Para hacerla lo más genérica posible, haremos que sea de tipo Object, realizando el casting correspondiente a la hora de implementar la clase concreta.

IObserver.cs

    public interface IObserver
    {
        // Metodo que sera invocado por el Subject
        // El objeto "o" seran los valores que el Subject le pasara al Observer
        void update(Object o);
    }

Lo siguiente que haremos sera codificar una interfaz que exponga los métodos propios de un notificador:

  • Un método para registrar un observer, que aceptará un IObserver como argumento.
  • Un método para eliminar la suscripción de un observer, que también aceptará un IObserver como argumento.
  • Un método para realizar la notificación a todos los observers que se encuentren suscritos al Subject.

ISubject.cs

    // Interfaz que expone los metodos de registro y eliminacion de observers, asi como para
    // la notificacion de los cambios de estado.
    public interface ISubject
    {
        void RegistrarObserver(IObserver o);
        void EliminarObserver(IObserver o);
        void NotificarObservers();
    }

A continuación codificaremos el Subject en sí: una clase que implementará la interfaz ISubject y que incluirá los métodos declarados en ésta. Además incluirá una serie de variables que almacenarán los distintos niveles (aceite, agua y presión de los neumáticos) junto a sus respectivas Properties, que permitirán modificar sus valores. En el método set de estas Properties será donde se realice la invocación del método NotificarObservers(), siempre y cuando el valor cambie, es decir, se comprueba si el nuevo valor es igual al anterior, y en caso contrario, se actualiza el estado y se notifica a todos aquellos Observers que se encuentren suscritos. Este método puede afinarse dependiendo del caso concreto (por ejemplo, notificando únicamente cuando el cambio suponga la superación de cierto umbral).

El constructor de la clase recibirá como parámetros los valores iniciales de los niveles de aceite, agua y presión de los neumáticos, además de instanciar el IList que almacenará los Observers suscritos a las notificaciones. Los métodos RegistrarObserver y EliminarObserver serán los encargados de añadir y eliminar suscriptores a la lista (previa comprobación de si éstos están ya en la lista o no). El método NotificarObservers() será tan sencillo como un bucle foreach que recorra todos los IObserver suscritos a las notificaciones, invocando su método update() pasándole el estado del objeto como parámetro.

Previamente hemos dicho que existen dos versiones del patrón:

  • Una versión push, que es precisamente la que definimos aquí: el propio Subject inyecta el estado en los Observers, pasándolo como parámetro al método Update().
  • Una versión pop, que como alternativa ofrece simplemente informar al Observer que se ha producido un cambio de estado, siendo responsabilidad de éste solicitar al Subject los nuevos valores.

MedidorSensores.cs

    public class MedidorSensores : ISubject
    {
        #region Estado

        // Atributos que modelan el estado
        private int nivelAceite;
        private int nivelAgua;
        private int nivelPresionNeumaticos;

        #endregion

        // Listado de observers
        IList suscriptores;

        #region Properties

        public int NivelAceite
        {
            get { return this.nivelAceite; }

            // Cada vez que se modifique el estado, se invocara el metodo NotificarObservers()
            set
            {
                if (this.nivelAceite != value)
                {
                    this.nivelAceite = value;
                    NotificarObservers();
                }
            }
        }

        public int NivelAgua
        {
            get { return this.nivelAgua; }

            // Cada vez que se modifique el estado, se invocara el metodo NotificarObservers()
            set
            {
                if (this.nivelAgua != value)
                {
                    this.nivelAgua = value;
                    NotificarObservers();
                }
            }
        }

        public int NivelPresionNeumaticos
        {
            get { return this.nivelPresionNeumaticos; }

            // Cada vez que se modifique el estado, se invocara el metodo NotificarObservers()
            set
            {
                if (this.nivelPresionNeumaticos != value)
                {
                    this.nivelPresionNeumaticos = value;
                    NotificarObservers();
                }
            }
        }

        #endregion

        #region Metodos de la interfaz ISubject

        // Constructor que creara un medidor con los valores iniciales de las presiones
        public MedidorSensores(int nivelAceite, int nivelAgua, int nivelPresionNeumaticos)
        {
            this.suscriptores = new ArrayList();
            this.nivelAceite = nivelAceite;
            this.nivelAgua = nivelAgua;
            this.nivelPresionNeumaticos = nivelPresionNeumaticos;
        }

        // Comprobamos si el observer se encuentra en la lista. En caso contrario,
        // lo incluye en la lista
        public void RegistrarObserver(IObserver o)
        {
            if (!suscriptores.Contains(o))
                suscriptores.Add(o);
        }

        // Comprobamos si el observer se encuentra en la lista. En caso afirmativo,
        // lo elimina de la lista
        public void EliminarObserver(IObserver o)
        {
            if (suscriptores.Contains(o))
                suscriptores.Remove(o);
        }

        // Recorre la lista de observers e invoca su metodo Update()
        public void NotificarObservers()
        {
            // Creamos un array con el estado del Subject
            int[] valores = { this.nivelAceite, this.nivelAgua, this.nivelPresionNeumaticos };

            // Recorremos todos los objetos suscritos (observers)
            IObserver observer;
            foreach(Object o in suscriptores)
            {
                // Invocamos el metodo Update de cada observer, pasandole el array con el estado
                // del subject como parametro.
                // Cada observer ya hara lo que estime necesario con esa informacion.
                observer = (IObserver)o;
                observer.update(valores);
            }
        }

        #endregion
    }

El último paso será la creación de las clases que implementen la interfaz IObserver. Crearemos dos clases: una de ellas simbolizará el display del vehículo, es decir, la pantalla de cristal líquido que muestra la información al conductor, además de la velocidad, revoluciones, llenado del depósito del combustible… Este Observer mostrará siempre los cambios producidos en los niveles, ya que su misión será la de informar al usuario en todo momento de los niveles actuales. Es el tipo más simple de Observer, ya que se limita a presentar la información recibida del Subject.

ObserverDisplay.cs

    public class ObserverDisplay : IObserver
    {
        #region Atributos

        // Atributos que modelan el estado
        private int nivelAceite;
        private int nivelAgua;
        private int nivelPresionNeumaticos;

        // Subject al que se encuentra suscrito el observer
        private ISubject subject;

        #endregion

        #region Constructores

        // El constructor suscribira el observer al subject
        public ObserverDisplay(ISubject subject)
        {
            this.subject = subject;
            subject.RegistrarObserver(this);
        }

        #endregion

        #region Metodos de IObserver

        public void update(object o)
        {
            // Comprobamos el tipo del objeto recibido como parametro
            int[] arrayInt = null;
            if (o.GetType().Equals(typeof(int[])))
                arrayInt = (int[])o;

            // Si es del tipo esperado (int[]) y del tamano esperado (3), actualizamos los
            // atributos
            if ((arrayInt != null) && (arrayInt.Length == 3))
            {
                nivelAceite = arrayInt[0];
                nivelAgua = arrayInt[1];
                nivelPresionNeumaticos = arrayInt[2];

                // Mostramos por pantalla los valores actuales
                MostrarValores();
            }
        }

        #endregion

        // Metodo que muestra los valores en el display
        private void MostrarValores()
        {
            Console.WriteLine("Nivel de Aceite: " + nivelAceite);
            Console.WriteLine("Nivel de Agua: " + nivelAgua);
            Console.WriteLine("Presion de Neumaticos: " + nivelPresionNeumaticos + "\n");
        }
    }

Como segundo Observer codificaremos una clase ObserverAlerta que, en lugar de informar siempre de los niveles actuales, sea capaz de enviar una alerta (sonora, remota, visual o de cualquier otro tipo) en caso de que los niveles superen ciertos umbrales superiores o inferiores. En caso contrario, no realizarán ninguna acción. Esto servirá para mostrar que un Observer realizará cualquier tipo de operación con estos datos, y también la suscripción de más de uno de estos elementos.

ObserverAlerta.cs

    public class ObserverAlerta : IObserver
    {
        #region Constantes

        // Niveles minimos y maximos
        private static readonly int MIN_ACEITE = 12;
        private static readonly int MAX_ACEITE = 45;

        private static readonly int MIN_AGUA = 300;
        private static readonly int MAX_AGUA = 550;

        private static readonly int MIN_PRESION = 120;
        private static readonly int MAX_PRESION = 350;

        #endregion

        #region Atributos

        // Atributos que modelan el estado
        private int nivelAceite;
        private int nivelAgua;
        private int nivelPresionNeumaticos;

        // Subject al que se encuentra suscrito el observer
        private ISubject subject;

        #endregion

        #region Constructores

        // El constructor suscribira el observer al subject
        public ObserverAlerta(ISubject subject)
        {
            this.subject = subject;
            subject.RegistrarObserver(this);
        }

        #endregion

        #region Metodos de IObserver

        public void update(object o)
        {
            // Comprobamos el tipo del objeto recibido como parametro
            int[] arrayInt = null;
            if (o.GetType().Equals(typeof(int[])))
                arrayInt = (int[])o;

            // Si es del tipo esperado (int[]) y del tamano esperado (3), actualizamos los
            // atributos
            if ((arrayInt != null) && (arrayInt.Length == 3))
            {
                nivelAceite = arrayInt[0];
                nivelAgua = arrayInt[1];
                nivelPresionNeumaticos = arrayInt[2];

                // Comprobamos que los valores no exceden los limites
                ComprobarAceite();
                ComprobarAgua();
                ComprobarNeumaticos();
            }

        }

        #endregion

        // Metodo que comprueba los niveles de aceite
        private void ComprobarAceite()
        {
            if (nivelAceite < MIN_ACEITE)
            {
                EnviarAlerta();
                Console.WriteLine(String.Format("NIVEL DE ACEITE DEMASIADO BAJO: {0}/{1}", nivelAceite, MIN_ACEITE));
            }
            if (nivelAceite > MAX_ACEITE)
            {
                EnviarAlerta();
                Console.WriteLine(String.Format("NIVEL DE ACEITE DEMASIADO ALTO: {0}/{1}", nivelAceite, MAX_ACEITE));
            }
        }

        // Metodo que comprueba los niveles de agua
        private void ComprobarAgua()
        {
            if (nivelAgua < MIN_AGUA)
            {
                EnviarAlerta();
                Console.WriteLine(String.Format("NIVEL DE AGUA DEMASIADO BAJO: {0}/{1}", nivelAgua, MIN_AGUA));
            }
            if (nivelAgua > MAX_AGUA)
            {
                EnviarAlerta();
                Console.WriteLine(String.Format("NIVEL DE AGUA DEMASIADO ALTO: {0}/{1}", nivelAgua, MAX_AGUA));
            }
        }

        // Metodo que comprueba la presion de los neumaticos
        private void ComprobarNeumaticos()
        {
            if (nivelPresionNeumaticos < MIN_PRESION)
            {
                EnviarAlerta();
                Console.WriteLine(String.Format("NIVEL DE PRESION DE NEUMATICOS DEMASIADO BAJO: {0}/{1}", nivelPresionNeumaticos, MIN_PRESION));
            }
            if (nivelPresionNeumaticos > MAX_PRESION)
            {
                EnviarAlerta();
                Console.WriteLine(String.Format("NIVEL DE PRESION DE NEUMATICOS DEMASIADO ALTO: {0}/{1}", nivelPresionNeumaticos, MAX_PRESION));
            }
        }

        // Metodo que envie la alerta
        private void EnviarAlerta()
        {
            // Este metodo podria enviar una alerta a la centralita del vehiculo que, por ejemplo,
            // forzaria a su detencion
            Console.WriteLine("ALERTA!!");
        }
    }

Finalmente, escribiremos el código necesario para mostrar el funcionamiento del patrón. Comenzaremos instanciando un Subject con unos niveles iniciales, para a continuación instanciar dos Observers (uno de display y otro de alerta) que recibirán como parámetro el Subject al cual se suscribirán de forma automática. A continuación alteraremos los valores del medidor de sensores y comprobaremos cómo los Observers son informados y actúan en consecuencia, mostrando siempre los cambios en los valores en el caso del display y mostrando únicamente las alertas cuando los valores se encuentran fuera de los límites para el caso del observer ObserverAlerta.

            // Creamos un medidor de sensores
            ISubject sensores = new MedidorSensores(20, 380, 200);

            // Creamos dos observers: un display y un emisor de alertas.
            // Se realiza la suscripcion a traves del constructor
            IObserver display = new ObserverDisplay(sensores);
            IObserver alerta = new ObserverAlerta(sensores);

            // Modificamos valores del subject. Los observers son automaticamente informados
            // y actuaran automaticamente
            ((MedidorSensores)sensores).NivelAceite += 10;
            ((MedidorSensores)sensores).NivelAceite += 10;
            ((MedidorSensores)sensores).NivelAgua += 100;
            ((MedidorSensores)sensores).NivelPresionNeumaticos -= 50;
            ((MedidorSensores)sensores).NivelAceite += 10;
            ((MedidorSensores)sensores).NivelAgua += 100;
            ((MedidorSensores)sensores).NivelAgua += 100;

Como vemos, siempre que se produce un cambio de estado, el display recibe la notificación y actúa en consecuencia. Cuando esta notificación llega a la alerta y los valores se encuentran fuera de los límites aceptables, además se emitirá una alerta.

Como alternativa, podemos probar a eliminar la suscripción a las alertas antes de que éstas rebasen los límites:

            // Modificamos valores del subject. Los observers son automaticamente informados
            // y actuaran automaticamente
            ((MedidorSensores)sensores).NivelAceite += 10;
            ((MedidorSensores)sensores).NivelAceite += 10;
            ((MedidorSensores)sensores).NivelAgua += 100;
            ((MedidorSensores)sensores).NivelPresionNeumaticos -= 50;

            // Eliminamos el registro de las alertas y aumentamos los niveles para comprobar que no
            // son informados
            sensores.EliminarObserver(alerta);
            ((MedidorSensores)sensores).NivelAceite += 10;
            ((MedidorSensores)sensores).NivelAgua += 100;
            ((MedidorSensores)sensores).NivelAgua += 100;

En este caso, únicamente el display realizará su trabajo, ya que hemos decidido eliminar la suscripción de ObserverAlerta a lo largo del programa, lo que implicará que este objeto no reciba a partir de ese momento las notificaciones del Subject.

IObservable e IObserver

En la práctica no será necesario definir la interfaz IObservable, ya que .NET ofrece por defecto las interfaces IObserver<T> e IObservable<T> (tal y como hacía con IEnumerable<T> con el patrón iterator) para facilitarnos un poco la vida. El parámetro <T> será la clase cuyo estado quiera observarse, y en este caso no tiene por qué implementar la interfaz ISubject, sino que será parametrizada en IObservable<T> y en IObserver<T>, es decir:

  • Se deberá crear una clase, por ejemplo MedidorSensores, que exponga una serie de Properties que se pretende monitorizar.
  • Se codificará una clase Suscriber<MedidorSensores> que implemente la interfaz IObservable<T> (siendo T en este caso MedidorSensores) que será la encargada de gestionar las suscripciones y enviar la información a los Observers. Su formato será el siguiente:
    public class Suscriber : IObservable<MedidorSensores>
    {
        public IDisposable Subscribe(IObserver<MedidorSensores> observer)
        {
            throw new NotImplementedException();
        }
    }
  • Se codificará una clase Provider<MedidorSensores> que implemente la interfaz IObserver<T> (siendo T en este caso MedidorSensores) que será la clase Observer e implementará los métodos OnCompleted, OnError y OnNext (siendo este último método el encargado de realizar las notificaciones).
    public class Provider : IObserver<MedidorSensores>
    {

        public void OnCompleted()
        {
            throw new NotImplementedException();
        }

        public void OnError(Exception error)
        {
            throw new NotImplementedException();
        }

        public void OnNext(MedidorSensores value)
        {
            throw new NotImplementedException();
        }
    }

Al final del artículo se indica un enlace en el que se podrá comprobar cómo utilizar estas interfaces aplicándolas a los datos de una estación meteorológica.

¿Cuándo utilizar este patrón? Ejemplos reales

Este patrón es útil en multitud de supuestos, pero el principal será cuando la modificación del estado de un objeto deba provocar cambio en otros objetos sin que sea necesario conocer nada acerca de la cantidad ni el tipo de dichos objetos.

Este patrón es la base de los eventos de la mayor parte de las interfaces de usuario: los Event Handlers se corresponderían con los Observers que esperan la notificación de un cambio de estado. Así, en un botón, definir un método para el evento OnClick hará que el control Button cambie su estado (por ejemplo, de no pulsado a pulsado), notificando a todos aquellos objetos que estén suscritos a él. De este modo, el Event Handler será notificado de este cambio de estado y ejecutará el código que haya sido programado para responder a dicho cambio de estado (por ejemplo, mostrar un mensaje cuando se pulsa un botón, o lo que es lo mismo, cuando el botón pasa de no pulsado a pulsado.

El patrón Observer también es la base del enlace de datos (data binding), por ejemplo en un control Gridview, que al obtener los datos de una fila de datos se encargará de dibujar la plantilla previamente configurada para mostrarle los datos al usuario.

Más adelante volveremos a este patrón, ya que es la base de uno de los patrones compuestos más utilizados hoy en día: el patrón MVC o Modelo Vista Controlador. El papel del patrón Observer en este patrón compuesto (el patrón MVC no es un patrón en sí, sino que es un patrón compuesto de varios patrones, al igual que una clase es una estructura de datos formada por otras estructuras de datos) será actuar de núcleo del mismo implementándose en el modelo. De este modo, los objetos interesados se mantienen actualizados cuando se produce un cambio de estado, haciendo que tanto la vista como el controlador estén débilmente acoplados con el modelo. Como adelanto, MVC hace uso de otros dos patrones ya vistos anteriormente: Composite (vista) y Strategy (controlador).

Fuentes:

10 comentarios

    1. Gracias, Matías.

      Las últimas semanas han sido bastante intensas y apenas he tenido tiempo de dedicarle tiempo al blog. No te quepa duda de que seguiré añadiendo patrones en la medida que mi tiempo me lo permita. Y por supuesto, el patrón MVC será el colofón a la serie de artículos sobre los patrones de diseño.

      Un saludo.

  1. Buenas, tus post son muy buenos y muy bien explicados. Me pregunto si me podés ayudar con una duda.

    Que pasa si en vez de enteros dentro de la clase ObserverDisplay tengo listas ? Como hago para notificar los subject cuando se realiza un Add a dicha lista ?

    Gracias !

    1. Hola, Emiliano, muchas gracias.

      Si observas el set de la propiedad NivelAceite, se realiza una invocación del método NotificarObservers() justo después de modificar el valor del entero.

      En tu caso deberías realizar lo mismo con la lista, es decir, encapsular el método «Add» de la lista dentro de otro método que adicionalmente notifique a los observadores, o bien extendiendo List para que el propio método Add (o cualquier método que quieres que realice una notificación) efectúe la invocación de este método (por ejemplo, mediante un event handler, tal y como se muestra en el siguiente enlace: http://goo.gl/048e0U).

      Un saludo.

  2. Muchas gracias por la explicación tan detallada y cuidada, hace mucho más fácil que se entienda este patrón para los que comenzamos a trastear con ellos.

  3. Excelente post, es algo realmente admirable encontrar informacion tan detallada acerca de un tema tan abstracto como los patrones de diseño y ademas en español !!

Deja un comentario