Design Patterns - Observer

TLDR: El Observer es un patrón que te permite definir un mecanismo de suscripción que notifica a múltiples objetos algún evento que esté pasando y que ellos estén observando.


El patrón de diseño Observer es usado para ofrecer un modelo de suscripción en el que los objetos se suscriben a un evento y estos obtienen una notificación cuando el evento ocurre. Esto se puede realizar dinámicamente en tiempo de ejecución, permitiendo tener una suscripción y des-suscripción a los eventos cuando se necesite.

¿Porque necesitamos el Observer?

Imagina que estás trabajando en un proyecto que contiene dos tipos de objetos, el Cliente y la Tienda. Los clientes están muy interesados en los nuevos productos que puede traer la tienda, en especial en sus últimos lanzamientos.

Los clientes pueden visitar la Tienda todos los días para preguntar si ya llegó el nuevo iPhone, pero hay un retraso en las entregas y en la tienda les notifican a los clientes que no está disponible.

Una posible solución, es que la tienda envíe cientos de correos electrónicos (En muchos casos, considerados como spam) a todos los clientes siempre que un nuevo producto llegue a la tienda. Esto podría evitar que el cliente perdiera su ida a la tienda, aunque esto podría molestar a los clientes que no están interesados en los nuevos productos.

Estas soluciones pueden llegar a ser ineficientes debido a que pueden causar que el cliente pierda tiempo valioso o la tienda gaste recursos notificando a el cliente que no está en realidad interesado.

Aquí es donde el patrón de diseño observer entra en acción, en el que sugiere agregar un mecanismo de suscripción y des-suscripción, para estar informados de las cosas que pasen, como cambios en las propiedades de algún objeto. Estas notificaciones deben tener información útil sobre el evento, para poder tener idea quien lo está desencadenando y cuales son los argumentos de este.

En definición, ¿que es el patrón Observer?

Un Observer es un objeto que desea ser informado sobre eventos que están ocurriendo en el sistema. La generación de la entidad ocurre cuando es un Observable

El problema en un ejemplo

Dándole continuidad al ejemplo pasado, supongamos que tenemos una tienda física y queremos que esta tienda tenga un mecanismo para suscribir a los clientes que estén interesados en el nuevo iPhone. Usando el patrón Observer, implementaremos una solución bastante practica que nos ayudará con el problema que tiene la tienda.

Para ello, vamos a crear la clase Event, en esta clase padre, vamos a manejar toda la lógica relacionada con las notificaciones y el manejo de los observadores o también llamadas ‘Personas interesadas’.

class Event {
  constructor() {
    this.handlers = new Map();
    this.count = 0;
  }

  subscribe(handler) {
    this.handlers.set(++this.count, handler);
    return this.count;
  }

  unsubscribe(idx) {
    this.handlers.delete(idx);
  }

  // (sender) Quien ejecutó los eventos?
  // (args) Argumentos adicionales (event args)
  notify(sender, args) {
    this.handlers.forEach((func, idx) => func(sender, args));
  }
}

En este caso, vamos a usar una lista tipo Map, que nos permite manejar los arrays de tipo clave - valor. Adicionalmente declararemos una variable count para realizar el contador del array.

En el primer método a implementar es el subscribe, cuyo parámetro será el evento a ejecutar (la función callback). Y el segundo método sería el unsubscribe que nos permite des-suscribirnos de los eventos cuando ya no estamos interesados.

El tercer método es el Notify que será el encargado de ejecutar las funciones de los observadores que han sido suscritas por medio de una iteración. Este método recibe dos parámetros, el sender que es la clase quien ejecutó el evento y los args que son los argumentos o datos adicionales.

Ahora, implementemos nuestra tienda usando la clase padre de Event.

class MarketStore extends Event {
  constructor(initInventory) {
    super();
    this.inventory = initInventory;
  }

  newInventoryArrived(newInventory) {
    this.inventory = newInventory;

    console.log(`MarketStore: Nuevo Inventario ha llegado:`, this.inventory);
    let iPhoneArrived = this.inventory.filter((i) => i === "iPhone 1k");
    if (iPhoneArrived.length > 0) {
      this.notifyIphoneUsers();
    }
  }

  notifyIphoneUsers() {
    this.notify(
      this,
      `El nuevo iPhone ha llegado, ` +
        `acercate a nuestras tiendas para llevártelo`
    );
  }
}

Aquí, en nuestra clase MarketStore podemos ver varias cosas. Primero que todo se realiza un extends de la clase Event, y en el constructor definimos nuestro inventario inicial.

Adicionalmente se crea una función llamada newInventoryArrive que nos permite cargar nuevo inventario a la tienda y este realizará una validación interna que nos permitirá determinar si ha llegado el nuevo iPhone 1k, para posteriormente notificar a los usuarios inscritos por medio de la función this.notify de la clase padre Event. En esta función se enviará el sender que sería la clase actual, y algunos argumentos.

Y por último crearemos la clase Person, que será el observador. Esta clase es bastante sencilla, pero tendrá la función update que veremos a continuación.

class Person {
  constructor(name) {
    this.name = name;
  }

  update = (sender, args) => {
    console.log(`Una notificación de ${sender.name} ha llegado!`);
    console.log(`${this.name}: ${args}`);
  };
}

Hay 2 cosas para tener en cuenta aquí. Se pasará por parámetros en el constructor el nombre que será usado posteriormente y se ha definido la función update como un arrow function, debido a que necesitamos que el this sea el mismo de la clase Person, esto para poder obtener el this.name de la instancia actual.

Al parecer todo está listo, para poder utilizar estas clases, lo implementaremos de la siguiente manera:

// Observador
let paul = new Person("Paul McCartney");

// Subject
let store = new MarketStore([]);
store.name = "Los Magníficos Store";
let paulId = store.subscribe(paul.update);
// Nuevo Suscriptor Map { 1 => [Function: update] }

store.newInventoryArrive(["iPhone 1k", "iPad pro"]);
// MarketStore: Nuevo Inventario ha llegado: [ 'iPhone 1k', 'iPad pro' ]
/* Llega la notificación */
// Una notificación de Los Magníficos Store ha llegado!
// Paul McCartney: El nuevo iPhone ha llegado, acercate a nuestras tiendas para llevártelo
store.unsubscribe(paulId);

store.newInventoryArrive(["iPhone 1k", "Magic Mouse"]);
// MarketStore: Nuevo Inventario ha llegado: [ 'iPhone 1k', 'Magic Mouse' ]
/* No han llegado más notificaciones */

Aquí podemos ver que el cliente, cuando ha comprado el nuevo teléfono, ya deja de estar interesado y llama el método store.unsubscribe(paulId). Así que cuando llega nuevo inventario con el nuevo teléfono, al cliente no le llegarán nuevas notificaciones.

Código completo del ejemplo: https://bit.ly/3cEUlth

Estructura del patrón Observer

En este patrón encontramos varios componentes que interactuaran entre si.

  1. El Subject, que en el ejemplo son las clases MarketStore cuyo padre es Event, es el encargado de mantener la lista de los observers o observadores, y enviarles notificaciones cuando el estado cambia. Adicionalmente, cualquier número de observadores puede observar un Subject.
  2. Los Observers, que en el ejemplo es la clase Person, básicamente es el objeto que tiene la función que será invocada cuando el evento del Subject ocurra.

Conclusiones

  • El patrón de diseño observer es usado cuando el cambio del estado de un objeto requiere cambiar otros objetos.
  • Este patrón debe ser usado cuando necesitas observar algunos objetos importantes en tu aplicación, pero por tiempo limitado o en específicos casos, debido a que cuenta con un sistema de suscripción dinámico. Los suscriptores o observadores pueden entrar e irse de la lista cuando ellos lo requieran y en tiempo de ejecución.
  • Este patrón te permite aplicar el principio de diseño Open/Close Principle, en el que puedes introducir nuevas clases de suscriptores (Observers) sin cambiar el código del publicador (Subject). Este patrón puede ser de gran utilidad cuando quieres partir una aplicación grande en pequeñas partes, y que no esté tan acoplada, permitiendo que el código sea más reutilizable.

Ejercicio

Se requiere que se construya un software con servicios de descarga de datos en segundo plano. Para en este servicio hay dos interesados:

  • El objeto que llenará la barra de progreso
  • El objeto que mostrará la notificación de descarga finalizada.

El servicio de descarga deberá notificar el progress y el estado success. Se requiere crear una solución aplicando el patrón Observer, se permita notificar el progreso a los dos interesados.

Se espera una salida de consola en la que se puedan diferenciar los interesados, por ej:

DownloadFileProcess: Descargando http://hola.com: 0%
DownloadFileProcess: Descargando http://hola.com: 10%
DownloadFileProcess: Descargando http://hola.com: 20%
DownloadFileProcess: Descargando http://hola.com: 30%
DownloadFileProcess: Descargando http://hola.com: 40%
DownloadFileProcess: Descargando http://hola.com: 50%
DownloadFileProcess: Descargando http://hola.com: 60%
DownloadFileProcess: Descargando http://hola.com: 70%
DownloadFileProcess: Descargando http://hola.com: 80%
DownloadFileProcess: Descargando http://hola.com: 90%
DownloadFileProcess: Descargando http://hola.com: 100%
DownloadFileCompleted: http://hola.com descargada correctamente

Estructura Inicial

Solución esperada

Recursos adicionales y bibliografia

Alexander Shvets - Dive into design Patterns

Erich Gamma, Richard Helm, Ralph Johnson, John M. Vlissides - Design Patterns Elements of Reusable Object-Oriented Software (1994)

The Observer Pattern - Addy Osmani - Learning JavaScript Design Patterns (2012)

https://www.dofactory.com/javascript/facade-design-pattern

https://loredanacirstea.github.io/es6-design-patterns/#observer