Decoradores en JavaScript con ES7 (ES2016)

En este último año se están conociendo numerosas características interesantes de las nuevas versiones de JavaScript, entre ellas los decoradores o decorators, que proporcionan una sintaxis muy sencilla y elegante para llamar a funciones de orden superior, así que he pensado que merecería la pena dedicarle una entrada del blog a esta nueva feature de ES7 o ES2016.

 

es2016 es7 decorador

 

¿Que es un decorador/decorator?

Pues no, un decorador no es un tipo vestido elegante y con acento cursi que pondrá más bonito tu código y te dará consejos sobre como organizar tu escritorio para conseguir un ambiente más zen, al menos en el contexto en el que hablamos. 

Hablando en serio, seguro que a todos os suena el patrón de diseño Decorator, pues bien, los decoradores se ajustan a este patrón, y podríamos resumirlos en pocas palabras: como que un decorador es una función que “envuelve” a otra para añadirle funcionalidad, de este modo podemos modificar clases y métodos en tiempo de diseño.

Esto no es cosa nueva en JavaScript, de hecho en ES5 no resultaba una tarea demasiado compleja, pero con ES6 y la implementación de clases era necesaria una forma sencilla -y elegante- para que varias clases y métodos pudieran compartir piezas de funcionalidad, así que con ES7 llegan los decoradores al rescate!

La sintaxis es bastante básica, seguro que a los que habéis trabajado con Phyton o con las últimas versiones de Java os resultará familiar.

@decorator(optionalParams)
class Foo { ... }
@decorator(optionalParams) 
function foo () { ... }

El nombre que sigue a la @ hace referencia a la función decoradora con el mismo nombre y esta función tomará como argumento (entre otros) la clase o la propiedad que va a ser “decorada” y nos la devolverá con la nueva funcionalidad. Son como píldoras de superpoderes para nuestras clases y propiedades. ¡Mola!

 

La función decoradora

La función decoradora puede recibir varios argumentos, targetnombredescriptor, y a su vez también puede devolver un descriptor al objeto de destino. Veamos estos argumentos en detalle.

@decorator

const decorator = (target, name, descriptor) => {
  ...
  return descriptor
}

Target

Es una referencia al objeto que se va a modificar, y dependiendo del uso puede ser el constructor del objeto o el prototipo.

Object { }

Name

El nombre de la propiedad o método que se va a modificar.

"name"

Descriptor

Se trata de un objeto que describe el objeto destino. Consta de varias propiedades; configurable, enumerable y writable, además de value, quizá la más interesante, ya que hace referencia a el valor o la propiedad que vamos a decorar.

Object {
  configurable: true,
  enumerable: true,
  value: function functionName () { ... },
  writable: true
}

Visto esto, tan solo nos queda ponernos manos a la obra y ver algunos ejemplos prácticos para entender bien como funcionan los decoradores.

 

Decorando like a boss

Para nuestros ejemplos vamos a crear una clase bastante básica Person con la que jugar un poco.

class Person {
  constructor (first = '', last = '') {
    this.first = first
    this.last = last
    this.age = 0
  }
	
  greeting () {
    return `Hi! I'm ${this.first} ${this.last}. Nice to meet you!`
  }
  
  grow (age) {
    for (let i = 0; i < age; i++) {
      this.age = i
    }
  }
}

const p = new Person('Edgar Bermejo')
console.log(p.greeting()) // Hi! I'm Edgar Bermejo . Nice to meet you!
p.grow(99)

Como veis no hay mucho que rascar. Además del método constructor al que se le pasan el nombre y el primer apellido como parámetros, nuestra clase Person tiene dos métodos más, greeting y grow.

Ahora imaginemos que necesitamos comprobar el tiempo que tarda nuestro método grow en ejecutarse, algo bastante común en el desarrollo de aplicaciones web y que seguro necesitaremos en más de una de nuestras funciones. Para conseguir nuestro propósito utilizaremos los métodos del objeto consoletime y timeEnd.

Si lo hiciésemos a la vieja usanza tendríamos que repetir código en cada una de los métodos que necesitamos debuguear, y eso es mal… Mejor vamos a crear un decorador que se encargue de esta tarea. Lo primero es escribir nuestro método decorador que será el que añada la nueva funcionalidad al método grow.

const timer = (target, name, descriptor) => {   
  // create a reference to the function
  let fn = descriptor.value
  // modify!
  descriptor.value = function () {
    console.time(name)
    let result = fn.apply(this, arguments)
    console.timeEnd(name)
  }
  // apply on property
  Object.defineProperty(target, name, descriptor);
}

Ya solo nos queda decorar las funciones que queramos debuguear con nuestro super decorador timer.

  ...

  @timer
  grow (age) {
    for (let i = 0; i < age; i++) {
      this.age = i
    }
  }
  ...

p.grow(99)

Y la consola nos “pinta” el tiempo en milisegundos que el método tarda en ejecutarse.

console-time-es7-decorator

 

Como veis tenemos una funcionalidad bastante interesante y una sintaxis de lo más elegante y práctica para decorar métodos, pero eso no es todo, también podemos aplicar decoradores a las clases de ES6. En este caso el método decorador solo recibirá como argumento el target, es decir, la referencia al objeto que será decorado.

Sigamos con la clase Person a la que ya hemos decorado uno de sus métodos. Esta persona, por el momento es un simple mortal que poca cosa puede hacer, pero vamos a intentar dotarla de superpoderes para que pueda hacer cosas increíbles como por ejemplo, volar. Para ello necesitamos añadirle un método fly y algunas propiedades.

const beSuperHero = (target) => {
  // we create a fly method
  let fly = () => {
    return `I'm flying!`
  }
  // and attach it to the target prototype
  target.prototype.isSuperhero = true
  target.prototype.canFly = true
  target.prototype.fly = fly
}

Como se ve en el código anterior es tan sencillo como añadir las propiedades y los métodos que queramos al prototype del objeto target.

Ahora decoremos nuestra clase Person y ¡hagamos que vuele!

@beSuperHero
class Person { ... }

const p = new Person('Edgar Bermejo')
console.log(p.isSuperhero)  // true
console.log(p.canFly)  // true
console.log(p.fly())  // "I'm flying!"

¿Sencillo, verdad? Pero no es demasiado elegante ni demasiado funcional… ¿Nos hacemos unos mixins?

 

Decoradores como functional mixins

El escenario ideal sería tener una función decoradora a la que se le pasen diferentes objetos como argumentos y que estos se apliquen a la clase que corresponda. La interfaz sería algo como esto:

@decorator(fanFuckingTasticObjects)

Dado que las clases de ES6 tan solo son un azucarillo sintáctico, y una clase viene a ser un objeto con su correspondiente prototype, será tan sencillo como añadirlos a dicho prototipo con Object.assign. Además, podemos utilizar los spread params de ES6 para hacer un poco más elegante la cosa.

const decorate = (...args) => {
  return (target) => {
    Object.assign(target.prototype, ...args)
  }
}

Bien, ahora tan solo necesitamos crear los objetos específicos con las propiedades y los métodos con los que queramos decorar nuestras clases. Volviendo a el ejemplo anterior, podríamos dotar de superpoderes a nuestra clase Person así:

const beSuperhero = {
  isSuperhero: true,
  canFly: true,
  fly: () => {
    console.log('Flying!!')
  }
}

@decorate(beSuperhero)
class Person { ... }

const p = new Person('Super Man')
console.log(p.isSuperhero)  // true
console.log(p.canFly)  // true
console.log(p.fly())  // "I'm flying!"

 

Compatibilidad y uso

Dado que estamos hablando de una característica experimental de ES7 la compatibilidad es bastante mala. Por suerte tenemos el plugin Decorator Transform de Babel para transformar los decoradores a “algo” legible para los motores JavaScript actuales.

$ npm install babel-plugin-transform-decorators

 

NOTA:

A día de hoy (Abril de 2016) el plugin de Babel transforma-decorators está obsoleto pero aún contamos con una opción para poder trabajar con decoradores, babel-plugin-transform-decorators-legacy.

Para utilizarlo:

npm install babel-plugin-transform-decorators-legacy

Y en el archivo .babelrc o bien si trabajamos con Webpack tendremos que añadirlo.

.babelrc

{
  "plugins": ["transform-decorators-legacy"],
  "presets": ["es2015", "react", "stage-0"]
}

webpack.config.js

query: {
  plugins: ['transform-decorators-legacy' ],
  presets: ['es2015', 'stage-0', 'react']
}

 

Sin duda los decoradores son una característica muy interesante de la nueva sintaxis JavaScript con numerosas aplicaciones y que simplifica enormemente las cosas. Estoy convencido de que a partir de ahora veremos muchas @ en nuestros códigos y en muchas librerías.

Siguiendo con el tema de ES7 o ES2016 en algunos días publicaré un artículo sobre otra de las características que considero más interesantes, las funciones asíncronas con async y await.

Por cierto, os dejo el enlace del pen con el que he estado trasteando para el ejemplo. 

Hasta la próxima!

 

Recursos y referencias