El Patrón de diseño decorador y decoradores en Javascript
Vamos a revisar un patrón que cada vez más se está utilizando en javascript y también vamos a ver cómo implementarlo.
El patrón decorador es muy utilizado en otros lenguajes de programación sin embargo no es tan utilizado en javascript. Desde que apareció la sintaxis de clases en ES6 y su utilización en el meta lenguaje Typescript ha ido introduciendo de forma recurrente. No obstante todavía se encuentra lejos de ser un estándar y tenemos que implementarlo apoyándonos en soluciones como Babeljs o en Typescript por ejemplo.
El patrón decorador es una solución a la necesidad de ampliar la funcionalidad de un objeto de forma dinámica mediante la composición. De esta forma podemos solucionar la extensión de funcionalidad sin recurrir a la herencia y a su nivel de acoplamiento. Vamos a revisar el concepto con un ejemplo para después ver cómo se utiliza con la sintaxis propuesta hasta ahora y en la cual se apoya el plugin de babeljs.
Vamos a crear una carpeta donde alojaremos nuestros archivos. A continuación en esta carpeta creamos un archivo index.js. También vamos a crear un archivo index.html con la siguiente estructura:
<!DOCTYPE html> <html> <head> <title></title> <meta charset='utf-8'> <meta http-equiv='X-UA-Compatible' content='IE=edge'> <script type="module" src='dist/index.js'></script> </head> <body> <mi-componente /> </body> </html>
Lo destacable es, por un lado, el script que importa el archivo index.js que está declarado como tipo “module”. Esto es porque lo estamos importando como un módulo y de esta forma podremos utilizar la sintaxis import/export dentro de él sin necesidad de paquetizar el código. El segundo es el custom tag mi-componente que definimos a continuación.
Ahora vamos a abrir el archivo index.js y vamos a codificar las siguientes líneas:
class MiComponente extends HTMLElement { constructor() { super() } } window.customElements.define('mi-componente', MiComponente);
Con esto hemos creado un web component. Pero este componente no tiene parte visual. Vamos a generarla modificando el constructor de esta forma:
constructor() { super(); let shadowRoot = this.attachShadow({ mode: 'open' }); shadowRoot.innerHTML =`<h1>Hola Mundo <h1>`;
Ahora ya tenemos nuestro componente con parte visual. Vamos a ver como ha quedado este simple webComponent. Pero antes tenemos que recordar una cosa y es que las últmas actualizaciones de los exploradores ya no nos dejan hacer peticiones de la página porque aplica políticas de cors. Puedes probarlo y te dará un mensaje en consola avisando que solo se pueden realizar peticiones por medio de schemes: http, data, chrome, chrome-extension, chrome-untrusted...
Hay una solución sencilla y es instalar de forma global la herramienta http-server. Es muy ligera y no necesita configuración. Para ello sólo tenemos que abrir una consola de comandas y escribir:
Una vez instalado y sin salir de la consola nos posicionamos en el directorio de nuestro proyecto y escribimos: http-server ./
Y tendremos levantado un servidor local en el puerto 8080. Si escribimos en el buscador localhost:8080/index.html nos aparecerá lo siguiente:
Repasando lo que hemos hecho podemos deducir que vamos a repetir las mismas lineas de código contenidas en el constructor para cada web component que queramos crear. Pero claro si creamos una clase padre que haga esto todos los componentes heredarán el mismo constructor y siempre construirán la etiqueta h1 con nuestro Hola Mundo. Si tuviésemos el control sobre la creación del objeto con el típico new "Object" lo tendríamos solucionado porque podríamos pasarle los parámetros dinámicos al constructor. Pero con los web components no instanciamos objetos, al menos en este ejemplo que estamos haciendo, si no que los creamos de forma declarativa en el html. La solución pasa por crear un middleware que recoja la clase inicial y el string que queremos convertir en html y devuelva una nueva clase con la propiedad shadowroot ya creada con nuestro html. Eso es lo que será nuestro decorador y lo implementamos así:
function Componente(obj) { return function (target) { class newTarget extends target { constructor() { super(); let shadowRoot = this.attachShadow({ mode: 'open' }); shadowRoot.innerHTML = obj.template; } } return newTarget; }
Ahora, como vamos a utilizar el decorador al que hemos llamado Componente. No necesitamos que nuestra clase implemente el shadowroot de forma explícita por lo que vamos a eliminarlo del constructor dejándolo así:
class MiComponente extends HTMLElement { constructor() { super() } }
Obtenemos una nueva clase utilizando el decorador de esta forma:
const miComponenteDecorado = Componente({template: '<h1>Hola Mundo <h1>'}) (MiComponente);
y cambiamos la definición del componente para utilizar la nueva constante declarada:
podemos recargar la vista del explorador y si lo hemos realizado de forma correcta vermos la misma vista.
Ahora que ya hemos visto cómo implementar el patrón decorador a nivel de una clase. También se puede implementar a nivel de método pero esto lo vamos a dejar para la última parte del tutorial ya que conlleva utilizar métodos reflexivos y puede que nos aleje un poco de la finalidad del tutorial.
Pues como hemos dicho antes este tipo de patrón se utiliza mucho en otros lenguajes como Java, Phyton, PHP, C#... y tiene estandarizada una sintaxis específica. En javascript no tenemos esta sintaxis como estándar pero existe como proposición para los próximos estándares. Por ello la forma más fácil de utilizarlo, y la más extendida ya que se encuentra en la mayoría de los proyectos reales, es utilizar Babeljs.
Para utilizarlo tenemos que crear un archivo package.json. Lo creamos con la sentencia:
Después instalamos Babeljs y su CLI
Una vez instalado vamos a instalar también el módulo del plugin plugin-proposal-decorators
Con todo ello instalado tenemos que crear el archivo de configuración de Babeljs. Este archivo se llamará babel.config.json:
{ "plugins": [ [ "@babel/plugin-proposal-decorators", { "legacy":true } ] ] }
Ya tenemos la configuración de babel. Al principio habíamos definido el script como un módulo. Pues esto nos va a permitir poder llevarnos el decorador a un archivo y dotarlo de capacidad de exportación para poder ser reutilizado en cualquier otro archivo. Esto es porque estamos utilizando una versión reciente del navegador pero no es compatible con versiones antiguas. Vamos a ello. Creamos un nuevo archivo llamado decorador.js y copiamos dentro nuestra función Componente.
export function Componente(obj) { return function (target) { class newTarget extends target { constructor() { super(); let shadowRoot = this.attachShadow({ mode: 'open' }); shadowRoot.innerHTML = obj.template; } } return newTarget; }; }
Le hemos añadido al principio la palabla exports. Ahora podemos importarlo en el archivo index.js
import { Componente } from './decorador.js';
y lo utilizamos según la sintaxis propuesta para los decoradores.
@Componente({ template: '<h1>Hola Mundo <h1>' }) class MiComponente extends HTMLElement { constructor() { super() } }
Como se puede apreciar, se utiliza escribiendo el carácter @ seguido del nombre del decorador. Luego entre paréntesis el objeto que espera recibir la función decoradora. Y con esto ya tenemos el decorador funcionando. Sólo queda cambiar la definición del componente y volver a utilizar la clase MiComponente y podemos recargar la página para comprobarlo.
Esta forma es mucho más limpia y visual. Y podemos dar solución a la optimización de implementaciones repetitivas donde no queremos o no podemos utilizar herencia de clases. En la imagen se puede ver cómo Babeljs ha resuelto la decoración de nuestra clase MiComponente.
A quién haya usado Angular le resultará familiar como hemos definido el template, y es que esta es la solución que utiliza el propio Angular para definir el template, providers , estilos y demás sin codificarlo dentro del constructor.
Pero todavía nos queda ver cómo utilizarlo con un método. Creamos un nuevo decorador en el mismo archivo index.js por no liarnos más.
function log(target, name, descriptor) { var oldValue = descriptor.value; descriptor.value = function () { console.log(`Calling "${name}" with`, arguments); return oldValue.apply(null, arguments); }; return descriptor; }
Este decorador va a a dehar una traza en consola cada vez que sea invocado. Esta es la forma propuesta pero puede que cuando llegue a ser estandar cambie. A diferncia de la función decoradora de clases, ésta tiene que recibir tres parámetros que son:
target: la clase ambito del método.
name: nombre del método.
descriptor: Este es un objeto que recoge las propiedades y valores del metodo sin ser el método. Es una propiedad reflexiva que se obtiene mediante Object.getOwnPropertyDescriptor
La clase la definimos así:
var _clase = class Clase { mimetodo(a, b) { return a + b } }
Como particularidad estamos recogiendo el objeto clase en la variable _clase. Esto lo hacemos para acceder a su prototype en la siguiente línea.
Object.defineProperty(_clase,log(_clase, 'mimetodo', Object.getOwnPropertyDescriptor(_clase.prototype, "mimetodo")));
Object.getOwnPropertyDescriptor(_clase.prototype, "mimetodo")));
Sin entrar en por qué utilizamos descriptor para cambiar los valores del método de la clase podemos ver que en esencia lo que hacemos es modificar la función asociada al método para que trace en consola un mensaje antes de ser ejecutada la función original.
Podemos probarlo recargando y revisando la consola:
Ahora vamos a utilizarlo con la sintaxis decorador.
var _clase = class Clase { @log mimetodo(a, b) { return a + b } }
Recargamos de nuevo y vemos el resultado.
Si quisiéramos pasar parámetros al decorador de método tenemos que hacer como con el de clase, envolverlo en una función que nos devuelva la función decoradora con los tres parámetros:
function log(parametro) { return function (target, name, descriptor) { var oldValue = descriptor.value; descriptor.value = function () { console.log(`Calling "${name}" with`, arguments); return oldValue.apply(null, arguments); }; return descriptor; } }