Patrones de diseño Observer en React.js

Patrones de diseño Observer en React.js

¿Qué es el patrón de diseño Observer?

Es un algoritmo hecho para un comportamiento en específico que resuelve uno de los problemas que se suele pasar al momento de programar.

Este patrón sincroniza un sujeto principal hacia otros objetos llamados observadores que se encuentran en cualquier otra parte de la aplicación. Sincroniza ambas partes para que estos otros objetos sean notificados de cualquier suceso que le pase al sujeto principal.

Por lo tanto este patrón permite crear un mecanismo de suscripción para notificar a los objetos suscritos de los eventos que le suceden al sujeto principal.

En el siguiente diagrama mostraré su comportamiento.

En el primer recuadro hay un sujeto, quien se encargará de guardar todos los objetos que se suscriban.  También se encargará de remover la suscripción y, por último, notificará a los observadores cuando algo ocurra al sujeto principal.

¿Qué problema resuelve?

El patrón de diseño observer atiende el problema que tenemos comúnmente en el comportamiento de asincronicidad. Muchas veces, sobrecargamos el método “useEffect” cuando lo tomamos para escuchar el comportamiento de ciertos eventos o datos que sufren cambios a corto tiempo.

Cuando abusamos del método “useEffect”, nuestra aplicación sufre cambios en  rendimiento, lo que afecta también a nuestro código, el cual se convierte en un desorden por la integración de muchos de este método mencionado.

El patrón observer llega para resolver este problema implementando el mecanismo de suscripción y notificación,  esto hace que nuestro código quede más ordenado, limpio, testeable y escalable a través de métodos precisos que actúan mediante la suscripción a un objeto principal. Si éste sufre un cambio, lo notifica a todos los suscriptores (observadores), quienes reaccionan a la notificación dada.

Ejemplo para aplicar este patrón

Imagina tener una aplicación de notificaciones donde llegan más de 10 cada segundo. Cuando éstas lleguen, reaccionaremos a este suceso en todos los componentes que tendremos en la vista de nuestra aplicación. Para este ejemplo pondremos cuatro secciones.

También tomemos en cuenta que nuestros componentes estarán a la expectativa cuando lleguen estas notificaciones y cada uno realizará una acción diferente. En este caso, cada componente mostrará un número random entre 1 y 1000.

Comúnmente, resolvemos lo anterior mediante un store, ya sea con redux, context, etc. y un “useEffect” en cada componente, para escuchar cuando las notificaciones se agreguen al store.

Aquí surgen varios problemas:

  • Todos los componentes estarán acoplados al store de redux.
  • Cada “useEffect” se dispara demasiadas veces por segundo. Esto hace que el componente pierda rendimiento y aún más si tenemos más de cuatro componentes que están a la expectativa de esos cambios en el store.
  • No es escalable. Cada funcionamiento que se quiera integrar en el futuro, estará acoplado al “useEffect”.
  • Será difícil testear funciones acopladas al método “useEffect”.    

¡La solución está en el patrón observer!

Ahora imagina tener un centro de datos (sujeto principal) donde guardarás todas las notificaciones que lleguen desde algún API. Este será el sujeto principal.

Cada componente que desee realizar un cambio/acción cuando llegue una notificación, se suscribirá al objeto principal, pero ahora ya no estará escuchando, sino que será notificado cuando llegue una notificación nueva y procederá a realizar las “n” acciones que desee realizar, cuando le notifiquen un suceso nuevo.

Con esta implementación se desacopla totalmente, por lo que sería escalable y testeable.



¿Cómo implementarlo?

Ahora veremos cómo implementar este patrón con el mismo problema.

Utilizaremos typescript para crear nuestro código con el tipado correcto.

Primero, crearemos nuestro proyecto en React.

Ahora crearemos dentro de nuestro proyecto la estructura de carpetas para nuestra solución.

La siguiente estructura de nuestra solución es la siguiente:

  • Components: En esta carpeta estarán ubicadas las cuatro secciones de nuestra página principal.
  • Hooks: Aquí estará la lógica independiente de cada sección. En este caso solo hay una, la cual retorna un número random para mostrarlo en cada componente.
  • Pages: Aquí tendremos nuestra página principal, la cual llamará a cada sección para mostrarla en la pantalla del navegador.
  • Utils: En esta carpeta pondremos nuestra lógica para tener nuestro sujeto, observador y una clase singleton para instanciar una sola vez nuestra clase sujeto.

¡Ya tenemos nuestra estructura! Ahora veremos nuestra implementación.

1) Crearemos ahora nuestro observador, el cual realizará una acción independientemente de cuando sea notificado.

utils/notification/observer.ts

Ya creamos nuestra interfaz de la estructura de nuestra clase.

2) Creamos nuestra clase Observer, misma que recibirá una acción en su constructor y, cuando éste sea notificado, ejecuta esa acción recibida en su actualización.

3) Crearemos nuestro sujeto principal mediante el que se  suscribirán los observadores y los notificará.

utils/notification/notification.ts

import { Observer } from "./observer";

 

/**

* The INotificationSubject interface declares a set of methods for managing subscribers.

*/

export interface INotificationSubject {

 // subscribe an observer to the INotificationSubject.

 subscribe(observer: Observer): void;

 

 // Detach an observer from the INotificationSubject.

 unSubcribe(observer: Observer): void;

 

 // Notify all observers about an event.

 notify(): void;

}

 

/**

* The Subject owns some important state and notifies observers when the state

* changes.

*/

export class NotificationSubject implements INotificationSubject {

 /**

  * @type {Observer[]} List of subscribers. In real life, the list of

  * subscribers can be stored more comprehensively (categorized by event

  * type, etc.).

  */

 public observers: Observer[] = [];

 public notificationsNumber: number = 0;

 

 /**

  * The subscription management methods.

  */

 public subscribe(observer: Observer): void {

   const isExist = this.observers.includes(observer);

   if (isExist) {

     return console.log("Subject: Observer has been attached already.");

   }

 

   console.log("Subject: Attached an observer.");

   this.observers.push(observer);

 }

 

 public unSubcribe(observer: Observer): void {

   const observerIndex = this.observers.indexOf(observer);

   if (observerIndex === -1) {

     return console.log("Subject: Nonexistent observer.");

   }

 

   this.observers.splice(observerIndex, 1);

   console.log("Subject: Detached an observer.");

 }

 

 /**

  * Trigger an update in each subscriber.

  */

 public notify(): void {

   this.notificationsNumber += 1;

   this.observers.forEach((observer) => {

     observer.update(this);

   });

 }

}

 



En este archivo, creamos nuestra interfaz INotificationSubject para declarar la estructura de nuestro sujeto principal, la cual tendrá 3 métodos: subscribe, unsubscribe y notify.

En la clase Notification subject, agregaremos dos propiedades observers y notificationsNumber.

El primero guardará a todos los observadores que se suscriban, quienes fungirán el rol de suscriptores. Un observador es un suscriptor cuando está guardado en este centro de datos.

Ahora notificationsNumber llevará el conteo del número total de notificaciones que han llegado al sistema.

Ahora se definen los siguientes métodos:

  • Subscribe: sirve para que los observadores se suscriban al sujeto y sean notificados en algún momento..
  • Unsubscribe: remueve al observador de los suscriptores.
  • Notify: se encarga de recorrer todos los suscriptores para notificarles, ejecutando el método update de cada observador (suscriptor).

4) Generaremos  nuestra clase singleton para crear, una sola vez, la instancia del sujeto principal.

Esta clase se extiende de NotificationSubject para contener todos sus métodos y propiedades (sujeto principal). Asimismo, con el método getInstance servirá para validar una sola instancia, creándose si no existe.  

De esta forma, tendremos un solo sujeto principal.

5) Ahora crearemos un archivo principal para alojar todos nuestras clases.

6) Es momento de hacer lo propio con nuestro hook, utilizado por todos los componentes.

Aquí intervienen dos elementos adicionales:

  • RandomNumber: Genera un número random entre dos números.
  • UseCount: Este es el hook que necesitamos para guardar dentro de un estado el número generado. El cual retorna el número del estado y la función que genera el número random.

Ahora generaremos nuestro componente Box, un contenedor con estilos para visualizar cada sección en la pantalla.

7) Seguiremos con nuestros componentes.

components/SectionOne/index.tsx

components/SectionTwo/index.tsx

components/SectionThree/index.tsx

components/SectionFour/index.tsx

Indico a continuación las funciones principales de estos componentes:

  • UseCount: Es el hook creado anteriormente para utilizarse en un momento dado. De éste obtenemos el randomCount y el count.
  • Notification: Se extrae la instancia del sujeto principal para suscribir y, en algún momento, darse de baja.
  • Observer: nuestro observador que le pertenece al componente definido, y le pasamos como acción la función randomCount, la cual se ejecutará cuando este observador sea notificado por el NotificationSubject.
  • UseEffect: es usado para suscribirse al sujeto principal (NotificationSubject) una sola vez cuando el componente sea montado. Cuando se desmonte el componente, la suscripción se da de baja.

Finalmente, se utiliza el count del estado y se integra dentro de la vista del componente.

¡Ya casi terminamos!

8) Trabajemos en nuestra vista principal.

  • UseState: Creamos un estado para guardar las notificaciones obtenidas.
  • Notification: Se obtiene la instancia NotificationSubject para dar uso de su propiedad notificationNumber y sus métodos subscribe y unsubscribe.
  • principalObserver: Se crea para guardar el total de notificaciones en el estado cada vez que entren notificaciones.
  • AsyncInterval: actuará como la petición de una API cada cierto tiempo (cada segundo) y notificará a los observadores de su respuesta.
  • UseEffect: se utiliza para suscribirse, remover su suscripción al momento que el componente sea desmontado y, por fin, ejecutar la función AsynInterval. Todo este proceso se realiza una sola vez (cuando el componente se ha montado).

Luego retorna una vista con el número total de notificaciones y las cuatro secciones distribuidas en la pantalla.

Aquí terminamos llamando nuestra vista principal en el archivo raíz del proyecto.

App.tsx

Ahora ya tenemos nuestra aplicación de notificaciones que contiene 4 secciones, las cuales realizan una acción independiente al ser notificadas de una nueva entrada en el sujeto principal.

Conclusión

De esta manera no sobrecargamos al método useEffect de React ni le damos un mal uso. Al contrario: implementando el patrón observer solucionamos el problema de sincronizar distintas partes de la aplicación hacia un objeto definido, lo que asegura a su vez que nuestro código sea entendible, escalable y testeable.

Ejemplo compartido en GitHub: https://github.com/dagamo/react-observer