Patrones de diseño en el frontend

0
20785

El mundo frontend es conocido por su gran volatilidad, sin embargo poco hacemos para que esta volatilidad no afecte a nuestros desarrollos. Nos importa últimamente estar más a la última del framework del momento que aprender a hacer nuestro código más mantenible. Así que este tutorial irá en pos de hacer una aplicación lo más «Frameworkless» posible.


Índice

El código podrás verlo en mi Github: https://github.com/cesalberca/frontend-patterns.

¡No olvides seguirme en Twitter!


Problema

Nuestro usuario tiene el siguiente problema: dado que su aplicación Web es altamente interactiva y hace uso de técnicas como carga de datos en diferido es necesario mostrar al usuario los distintos estados de la aplicación.

Inicialmente, al no haber cargado nada se mostrará una luz en gris: Sin cargar

En el momento en que comienza una petición se mostrará una luz en azul y se bloqueará el botón:

Cargando

Si la petición actual ha ido bien mostrar una luz en verde, los usuarios resueltos y desbloquear el botón:

Éxito

Y si no, una luz en rojo y desbloquear el botón:

Error

Si se vuelve a peticionar algo se volverá a mostrar la luz azul.

El usuario prevé que querrá añadir algún aviso sobre algunas peticiones que sean destructivas, como el borrado de una entidad, y además querría notificar al usuario de éstas de alguna forma.

Por supuesto nuestro usuario necesita que todas las peticiones por defecto se comporten así, pudiendo en alguno lugares añadir gestiones más especiales para capturar errores más específicos.


Solución

La solución que he ideado parte de un enfoque más simple, sobre el que he ido iterando para poder extender fácilmente mi código para adaptarme a nuevas funcionalidades. Para ello he usado una serie de patrones de diseño que me ayudaran a gestionar de mejor forma el código. Usaremos TypeScript y React.


Chain of responsibility

Le gestión de una petición asíncrona tiene que ir pasando por una serie de estados: inicio de la petición, respuesta de la petición que a su vez se divide en: petición resuelta con éxito y petición fallida. Y además, este ciclo es lineal. Incluso se podría decir que es una cadena.

Para este tipo de estructuras existe un patrón de diseño llamado chain of responsability que lo que pretende es gestionar el procesamiento de objetos siendo cada objeto el que tenga la lógica de procesado. Es decir, este patrón nos puede ahorrar un montón de if y elses haciendo cumplir el principio de Open/Closed de SOLID (abierto a la extensión, cerrado a la modificación) como veremos más adelante.

¡Así que vamos a ello! Vamos a empezar por la interfaz Handler:

La interfaz recibe un genérico, con lo cual esta interfaz nos valdría para otras cadenas.

Esta interfaz describe dos métodos. El primero es una función que invocará el siguiente handler de la cadena, pudiendo pasar un objeto context. Este context nos servirá para ir realizando las operaciones pertinentes sobre la petición o el estado de la aplicación.

El método setNext nos permite definir el siguiente objeto de la cadena, recibiendo a su vez un Handler.

Ahora bien, ¿cómo sería la implementación de un Handler? Pues sería algo tal que así:

En el método next tendremos la gestión del comienzo de una petición, dado que tiene que pasar lo siguiente:

  • Poner el estado a cargando
  • Invocar la función que hará la petición (es una callback para conseguir una evaluación lazy)
  • Invocar al siguiente elemento de la cadena

El RequestHandlerContext es una interfaz con lo siguiente:

Es aquí donde definimos lo que tendrá el objeto context.

También vemos que se da un valor por defecto al nextHandler que es el RequestEmptyHandler. Este handler vacío lo que hace es… nada. Este es el handler por si en algún momento se intenta llamar al next del último handler. ¿Su implementación? Muy sencilla:

Como hemos dicho antes, después de la petición hay una respuesta, que a su vez sería un Handler. Aunque este Handler es a su vez un poco especial, dado que debe poder gestionar una respuesta con éxito o una respuesta fallida:

Aquí vemos varias cosas, dentro de este Handler tenemos un RequestErrorHandler y un RequestSuccessHandler, y es en el next donde se determina qué camino ha de seguir la cadena. Una vez se ha decidido dicho camino se invoca al método next.

Cómo todos los Handlers implementan la misma interfaz aquí vemos la magia del [polimorfismo](https://en.wikipedia.org/wiki/Polymorphism_(computer_science)
, donde a esta clase poco le importa cuál sea el siguiente Handler, este se preocupa de elegir el camino correcto, ya serán el resto de Handlers quienes determinen qué tienen que hacer (esto hace que sigamos la S de SOLID – Single responsibility principle).

Y además vemos algo muy interesante, en el finally decimos que context.state.currentState.isLoading se ponga a false. Pero si vemos un poco más arriba, hacemos await de la llamada al siguiente handler, lo que quiere decir esto que estamos mutando el estado una vez se ha ejecuta el siguiente handler. Esto nos puede venir de perlas si no quisiéramos parar la ejecución del programa o si quisiéramos ejecutar algo a posteriori a modo de «limpieza». En este caso una vez resuelto la petición con éxito o con error, queremos que se cambie el estado a cargado y no antes.

La clase de éxito de la petición es RequestSuccessHandler:

Y la de error es RequestErrorHandler:

Aquí vemos dos hasError, la diferencia es que uno lo usamos en el estado de la aplicación en sí y otro lo usamos para gestionar la respuesta de la petición.

Ahora nos queda la última pieza… ¿Quién orquesta todo? Pues el RequestHandler:

Aquí básicamente creamos la cadena de Handlers, les decimos a cada uno cuál es su siguiente elemento de la cadena y exponemos a los clientes un método sobre el que pueden iniciar la cadena, que es el método trigger, el cual recibirá una función que retorna una promesa, que es la petición en sí. El trigger además retorna los valores o un error.

Lo que parece un objeto Request realmente es un namespace de TypeScript, donde agrupo las cosas que tienen que ver con el objeto petición (Request):


Proxy

Ahora tenemos un problema, el lector atento habrá visto que en el objeto context hemos empezado a cambiar unas propiedades del objeto state tal que así:

Esto es parte de la solución que implementaremos ante el problema de recargar la vista cuando un valor cambia. Porque según nuestra historia de usuario tenemos que representar varios estados del cargando de forma dinámica.

Esto podemos hacerlo con un Proxy de JavaScript, que curiosamente implementa el patrón Proxy por debajo, que será donde guardemos el estado. En este Proxy, podremos capturar todas las mutaciones de sus valores. Y teniendo esto, solamente nos hace falta conectar los componentes de nuestra aplicación con este estado, que es de la siguiente forma:

Ahora cada vez que mutemos el estado del StateManager podremos lanzar acciones, que serán observadas.


Observador

Ahora bien, necesitamos exponer al mundo una forma de poder observar estos cambios en el estado. Ahí entra el patrón observador. Empezamos por el sujeto:

El sujeto es aquella entidad que será observada. Tendrá un array de observadores y un método que permitirá registrar un nuevo observador y otro método para notificar a todos los observadores de que algo ha cambiado.

Y el observador:

El observador será notificado mediante el método notify.

Y si lo hilamos todo junto al StateManager:

Nos quedaría únicamente definir los observadores, pero eso lo veremos más adelante.

El tipo State no es más que una interfaz con el siguiente contenido:


Singleton

Si pensamos el caso de uso del estado, nunca tendrían sentido dos instancias o más de StateManager. Para evitar crear instancias de más tenemos el patrón Singleton. Para ello añadimos un campo a la clase llamado instance cuyo valor por defecto será null:

Añadimos un getter del campo privado _instance dónde gestionamos su creación una única vez:

Por último cambiamos la visibilidad del constructor de pública a privada, para que únicamente se pueda obtener la instancia de la clase por el getter instance.

El resultado sería el siguiente:


React

Para la aplicación he optado por usar React, aunque hemos hecho el código de tal forma que la lógica de la aplicación no está acoplada con ningún framework.

Tenemos el componente Light que tendrá el siguiente contenido:

Y tendremos por encima un LightContainer, este componente es un denominado «contenedor». Los contenedores y componentes son un patrón de diseño que aplica a frameworks que se basan en componentes y la diferencia es que los contenedores son más listos que los componentes, ya que gestionan el estado y orquestan los componentes. Se empezó a usar en el front a raíz de este artículo de Dan Abramov.

Este patrón nos recuerda mucho al patrón Mediator del GoF, donde un objeto es el encargado de gestionar las dependencias entre muchos objetos. En este caso el mediador será el contenedor y los componentes serán las dependencias. La forma de comunicación será basada en props y callbacks.

El contenido del contenedor es el siguiente:

Aquí vemos varias partes interesantes, estamos usando el API de Context de React para consumir un objeto context y parece que este nos provee de un fakeRepository que veremos más adelante. Vemos que estamos renderizando el componente Light y le pasamos directamente un state con getState() y este a su vez accede por props a un tal stateManager.


Context

El API de context nos va a hacer las veces de inyección de dependencias para poder cumplir uno de los principios SOLID, el de la D que es dependency inversion, que dictamina que no deberíamos depender en concreciones si no en abstracciones. ¿Cómo logramos esto? Pues resulta que fakeUserRepository es una interfaz y tiene la siguiente pinta:

Y Repository es otra interfaz del siguiente tipo:

En esta interfaz podríamos definir métodos de acceso de entidades, por ejemplo: findOne, delete o update. Y luego cada interfaz de tipo repositorio ya definiría métodos más concretos como: findUserByName.

Y por tanto nos queda ver la implementación de esta interfaz:

Si vemos que la lógica de muchos repositorios es idéntica, podríamos crearnos un GenericHttpRepository que nos diese esa funcionalidad común.

Y aquí ya empezamos a ver todo hilado, este FakeUserHttpRepository ya usa por debajo nuestro famoso RequestHandler, siendo este el que gestiona el ciclo de vida de la petición.

La utilidad wait no es más que un setTimeout promisificado:

Ahora nos queda meter esto en el contexto de React, ahí entra el rootContainer:

Pasamos de una abstracción a una concreción, y esto es muy potente, porque imaginemos que queremos implementar un sistema de caché en local storage, lo único que tendríamos que crear es un repositorio FakeUserLocalStorageRepository y dinámicamente cambiar la implementación entre el FakeUserHttpRepository y el anterior, siendo completamente transparente para el consumidor.

El consumidor al final le da igual de dónde vengan los datos, él quiere los usuarios, ya será en otro sitio de dónde tiene que sacarlos. Además, si el día de mañana quisiéramos migrar a GraphQL lo único que tendríamos que hacer sería añadir otro repositorio, cumpliendo así otro de los principios de SOLID, el de la O, que es Open/Closed, lo que quiere decir que si añadimos funcionalidad no tenemos que tocar código antiguo si no añadir más código.

Y nos queda el punto inicial de la aplicación, el Aplication.tsx:

Aquí proveemos del contexto y la pasamos al stateManager el estado, que, como es un singleton pues será el mismo estado siempre.


Observadores

En un capítulo anterior hemos desarrollado un StateManager que era un sujeto, pero en ningún momento hemos definido quiénes se iban a suscribir a esa parte del estado. ¿Quiénes van a ser los suscriptores? Pues los componentes de React, para ello a nuestro componente LightContainer le diremos que implementa la interfaz Observer, que cuando se monte tiene que registrarse y que implementa un método notify que llama el forceUpdate de React.

Quedando al completo así:

El forceUpdate no hace que se renderice de nuevo todo el árbol, React sigue aplicando el diffing para renderizar solamente aquello que ha cambiado.


Nueva feature

Ahora veremos cuánto cuesta añadir nueva funcionalidad. El usuario ahora quiere que se muestre un botón que permite borrar a los usuarios. Pero antes de ello, se debe mostrar un mensaje advirtiendo al usuario que se va a proceder con una operación destructiva. El usuario va a poder cancelar la operación antes de 2.5 segundos. Si no hace nada se procederá con la petición destructiva.

Primero añadimos unos nuevos estados en State: hasWarning y userHasCanceledOperation. La interfaz quedaría así:

Dentro del StateManager le damos un valor vacío y le decimos que cuando hay un estado vacío debe poner el hasWarning y el userHasCanceledOperation a false:

Creamos una clase RequestWarningHandler:

Si el usuario no acepta el warning en un marco de 2 segundos y medio no comenzará la petición, ya que se irá al RequestEmptyHandler. Además, vemos una función waitUntilOr que tiene el siguiente contenido:

Necesitamos esta función para evitar que al cancelar el borrado de usuarios varias veces dentro del marco de 2.5 segundos se cree otro intervalo.

Modificamos el método trigger de la clase RequestHandler y refactorizamos un poco para que quede más claro las distintas rutas que se pueden tomar:

Añadir al repositorio genérico Repository el método de deleteAll:

Y lo implementamos en el FakeUserHttpRepository:

Por último, añadimos al contenedor el botón. Ahora nuestro contenedor tendrá un estado interno que nos dirá si se debe mostrar un warning:


Conclusión

Hemos visto un montón de patrones (Singleton, Observador, Chain of responsibility, Proxy, Mediator), junto con separación de capas con repositorios, estado y componentes. Hemos comprobado que si nuestro código cumple SOLID será más fácil lidiar con el, más mantenible y aceptará mejor el cambio.

El código podrás verlo en mi Github: https://github.com/cesalberca/frontend-patterns.

¡No olvides seguirme en Twitter!

Dejar respuesta

Please enter your comment!
Please enter your name here