Programación Avanzada

Patrones de diseño

Patrón de diseño Observador (Observer)

Introducción

Existe multitud de casos (sistemas de monitorización, interacción con interfaces gráficos de usuario, etc.) donde un objeto cliente necesita conocer los cambios que se han producido en otro objeto.

Una posible solución es que el cliente interrogue, cada cierto tiempo, si se ha producido algún cambio en su objeto de interés. Pero esta solución es altamente ineficiente.

Una solución mejor es que el objeto de interés envíe una notificación al objeto observador sólo en el caso en que se produzca un cambio en el primero.

Bibliografía

Contenidos

  1. Mi primera vez tomando un avión
  2. Algunos ejemplos.
  3. Escribamos la aplicación de notificación de vuelos.
  4. Definición del patrón de diseño Observador.
  5. Ejemplos en el paquete de Java.
  6. Otros ejemplos.
  7. Resumen.
  8. Recursos en Internet.

Mi primera vez tomando un avión

Los hechos descritos es este capítulo son ficción.

La primera vez que tuve que coger un vuelo, al llegar al aeropuerto preguntaba a todo le mundo:

  • ¿Por favor, el vuelo hacia...?
  • Acérquese a su puerta de embarque y allí le dirán.
  • De acuerdo gracias.

Y ya en la puerta de embarque:

  • Por favor, ¿A qué hora sale el vuelo hacia...?
  • Aún no se conoce la hora.
  • Gracias.

Mi primera vez tomando un avión

Y diez minutos más tarde.

  • ¿Se sabe ya cuando saldrá el vuelo hacia...?
  • No, aún no, pero no se preocupe nosotros le avisamos.
  • Ah, gracias.

Unos instantes después:

  • El vuelo con salida hacia... efectuará su salida...

Bien, pues en eso consiste precisamente el patrón de diseño Observador, no te preocupes por enterarte de que algo ha ocurrido, dinos que estás interesado, y cuando ocurra, nosotros te avisamos.

Algunos ejemplos

Clientes gráficos

Imagina una aplicación que muestra los datos, en tiempo real, de la cotización de valores en bolsa.

El cliente gráfico no interroga continuamente si una cotización ha variado, sino que, si una cotización varía, se informa automáticamente al cliente gráfico para que pueda actualizar su estado.

Fíjate que aparece de nuevo la idea de inversión de control, el inicio de la actividad no parte del cliente si no de quién proporciona los datos.

Algunos ejemplos

Monitorización

Imagina que estás construyendo un sistema de monitorización para un horno de cocción de azulejos. Si la temperatura se eleva por encima de cierto umbral debes disparar una alarma, hacer una llamada de emergencia y almacenar el estado actual del sistema.

La alarma, el sistema de llamadas y el sistema de almacenamiento son los clientes que desean ser informados cuando se produzca la situación anómala: temperatura elevada.

Escribamos la aplicación de notificación de vuelos

Nuestra aplicación debe funcionar de tal modo que cualquier viajero se pueda suscribir a un vuelo, para recibir las notificaciones que se vayan generando sobre él.

Tenemos estas tres condiciones iniciales:

  1. Un viajero sólo se podrá suscribir a las notificaciones generadas por un vuelo.
  2. Un vuelo puede tener suscritos cero a más viajeros.
  3. Cada vez que en un vuelo genere una notificación esta ha de llegar a todos los viajeros suscritos.

Escribamos la aplicación de notificación de vuelos

Empecemos con los atributos y constructor del Vuelo:

public class Vuelo {
    private String codigoDestino;
    private List<Viajero> viajeros;
    private String ultimoSuceso;

    public Vuelo(String codigoDestino) {
        super();
        this.codigoDestino = codigoDestino;
        viajeros = new ArrayList<Viajero>();
        ultimoSuceso = "";
    }
    ....

Escribamos la aplicación de notificación de vuelos

Ahora los atributos y constructor del Viajero:

public class Viajero {
    private String nombre;
    private Vuelo vuelo;

    public Viajero(String nombre, Vuelo vuelo) {
        super();
        this.nombre = nombre;
        this.vuelo = vuelo;
    }
    ...

Escribamos la aplicación de notificación de vuelos

Completemos la implementación de Vuelo.

Un Viajero se suscribe a las notificaciones de un vuelo con:

public void suscribirObservador(Viajero viajero) {
    viajeros.add(viajero);
}

Y elimina sus suscripción con:

public void eliminarObservador(Viajero viajero) {
    viajeros.remove(viajero);
}

Escribamos la aplicación de notificación de vuelos

Cada vez que algo ocurre, hay que notificarlo a todos los viajeros suscritos:

public void notificarObservadores() {
    for(Viajero viajero: viajeros)
        viajero.notificar();
}

Antes de pasar adelante, ¿Por qué simplemente notificamos y no enviamos además lo que ha ocurrido: ultimoSuceso?

Lo vemos después de acabar la presentación del patrón.

Escribamos la aplicación de notificación de vuelos

Completemos la implementación de la clase Vuelo con un par de métodos de utilidad.

public String getUltimoSuceso() {
    return codigoDestino + ":" + ultimoSuceso;
}

public void setUltimoSuceso(String ultimoSuceso) {
    this.ultimoSuceso = ultimoSuceso;
    notificarObservadores();
}

Pasemos a la clase Viajero.

Escribamos la aplicación de notificación de vuelos

La clase Viajero tiene poco código:

public void notificar() {
    System.out.println(nombre + "<-- Notificar: " +
        vuelo.getUltimoSuceso());
}

No necesita nada más.

Escribamos la aplicación de notificación de vuelos

Antes de hacer alguna mejora probémoslo:

Vuelo vuelo = new Vuelo("IB123 destino París");
Viajero oscar = new Viajero("Oscar", vuelo);
vuelo.suscribirObservador(oscar);
Viajero pepe = new Viajero("Pepe", vuelo);
vuelo.suscribirObservador(pepe);
vuelo.setUltimoSuceso("Llegada del vuelo.");
vuelo.eliminarObservador(pepe);
vuelo.setUltimoSuceso("Salida de viajeros");

Escribamos la aplicación de notificación de vuelos

Oscar ← Notificar: IB123 destino París:Llegada del vuelo.

Pepe ← Notificar: IB123 destino París:Llegada del vuelo.

Oscar ← Notificar: IB123 destino París:Salida de viajeros

Escribamos la aplicación de notificación de vuelos

Objetivo cumplido, pero intentemos mejorar el código, ya que esta idea parece potente. Aseguremos que el método notificar() lo define la clase Viajero definiendo el siguiente interface Observador.

public interface Observador {
    void notificar();
}

Ahora Viajero quedará como:

public class Viajero implements Observador {
    ...

Escribamos la aplicación de notificación de vuelos

E implementemos la clase que es el Observado

public class Observado {
    private List<Observador> observadores;
    public Observado() {
        super();
        observadores = new ArrayList<Observador>();
    }
    public void suscribirObservador(Observador observador) {
        observadores.add(observador);
    }
    public void eliminarObservador(Observador observador) {
        observadores.remove(observador);
    }
    public void notificarObservadores() {
        for(Observador observador: observadores) observador.notificar();
    }
}

Escribamos la aplicación de notificación de vuelos

Fíjate que la clase Observado sólo tiene un cometido: informar a todos los observadores cada vez que se produzca un evento.

Una única responsabilidad, un único motivo para el cambio.

Los detalles particulares los implementarán sus clase hijas, como por ejemplo Vuelo.

Escribamos la aplicación de notificación de vuelos

Para acabar, la clase Vuelo quedará como:

public class Vuelo extends Observado {
    private String codigoDestino;
    private String ultimoSuceso;
    public Vuelo(String codigoDestino) {
        super();
        this.codigoDestino = codigoDestino;
        ultimoSuceso = "";
    }
    public String getUltimoSuceso() {
        return codigoDestino + ":" + ultimoSuceso;
    }
    public void setUltimoSuceso(String ultimoSuceso) {
        this.ultimoSuceso = ultimoSuceso;
        notificarObservadores();
    }
}

Definición del patrón de diseño Observador

Define una dependencia de uno a muchos entre objetos, de forma que cuando un objeto cambie de estado se notifique y se actualicen automáticamente todos los objetos que depende de él.
Patrones de diseño
Erich Gamma et al.

Definición del patrón de diseño Observador

Este es el diagrama UML de este patrón:

Definición del patrón de diseño Observador

Y esta es el caso particular de nuestro ejemplo:

Definición del patrón de diseño Observador

Ventajas:

  1. Acoplamiento abstracto entre Observador y Observado: Lo único que conoce el observador de sus clientes observadores es que implementan una cierta interfaz.
  2. Comunicación mediante difusión: los observadores están suscritos a eventos, cuando este se produce, la información se difunde entre todos los observadores.

Desventajas:

  1. Un aparente cambio inofensivo en el Obervado puede desencadenar una cascada de notificaciones hacia los Observadores.

Avísame de que algo ha ocurrido, ya me preocupo yo por saber qué

A esta técnica se le llama el Principio de Hollywood, no nos llames, ya te llamaremos nosotros.

Avísame de que algo ha ocurrido, ya me preocupo yo por saber qué

Ahora pasemos a contestar la pregunta que dejamos pendiente: ¿Por qué simplemente notificamos y no enviamos además lo que ha ocurrido: ultimoSuceso?

La primera respuesta, contradictoria, es que sí que lo podíamos haber enviado.

La matización es: siempre que sepamos qué es exactamente lo que el cliente espera de nosotros.

Pero si no estamos seguros de lo que el cliente espera de nosotros es mejor que, simplemente, le informemos de que algo ha ocurrido, y que sea el cliente quien nos pida lo que necesita.

Ejemplos en el paquete de Java

Los paquetes Java relacionados con la gestión de interfaces gráficos de usuario están plagados por el uso de este patrón de diseño.

La idea básica es que los componentes gráficos, por ejemplo un botón, lanzan eventos cada vez que el usuario interacciona con ellos, y estos eventos llegan todos los clientes que quieran estar informados de lo ocurrido.

Los eventos que puede lazar un botón son muchos: se ha hecho click, el cursor del ratón ha entrado en su area, ha salido de su áera, etc. Los clientes se suscriben sólo a los eventos que les interesa.

Veremos con más detalle este procedimiento en el tema de creación de interfaces gráficos de usuario.

Otros ejemplos

Las colas de mensajería (broker) son otro ejemplo de implementación del patrón de diseño Observer, también llamado Publicación/Subscripción (Publish/Subscribe).

Fuente: wikipedia.

Otros ejemplos

Hay clientes del broker que producen mensajes.

Hay clientes del broker que consumen mensajes.

Cada consumidor dice cual o cuales son sus tópicos de interés cuando se registra en el broker.

Cuando un productor envía un mensaje, el broker sabe a qué clientes debe enviarlo.

Los productores y consumidores no se conocen entre ellos. Productores y Consumidores están totalmente desacoplados.

Otros ejemplos

Resumen

El patrón de diseño Observer desacopla completamente los productores y consumidores de mensajes.

Los consumidores se suscriben para recibir notificaciones.

Los productores envían notificaciones a los consumidores.

El ejemplo típico es la respuesta a los eventos que se producen en una aplicación con interfaz gráfica de usuario.

Recursos en Internet