Hamburger Icon
Uso de decoradores en JavaScript

Uso de decoradores en JavaScript

Ya hace mucho que en JavaScript podemos decorar funciones y clases, aunque no del mismo modo en que lo haríamos en otros lenguajes. Antes de entrar en materia, es necesario entender qué es un decorador.

Qué es un decorador

Una decorador es una función que hace lo siguiente, en este orden:

  1. toma una función como parámetro
  2. extiende el comportamiento de la función que recibe
  3. devuelve la función extendida

Esto es posible en JavaScript porque las funciones son first class citizens o ciudadanos de primera clase. En cualquier lenguaje de programación se usa este término para todas aquellas entidades compatibles con las operaciones que suelen estar disponibles por otras entidades, incluyendo ser pasadas como argumento, ser devueltas en una función y asignaciones a variables.

Las funciones en JavaScript pertenecen a este tipo de entidad ya que pueden ser pasadas como argumento a otras funciones y pueden ser usadas como valor de retorno dentro de otra función (entre otras opciones, pero estas son las que interesan en este post).

En otras palabras, un decorador es una función que añade características a otra función.

Decorando funciones en JavaScript

Vamos a ver ya el primer ejemplo. En primer lugar, tenemos estas dos funciones:

const bienvenida = (mensaje) => {
  console.log(`Ya he llegado, así que digo... ${mensaje}`)
}

const despedida = (mensaje) => {
  console.log(`Ya me voy, de modo que... ${mensaje}`)
}

bienvenida("¡Buenos días!") // Ya he llegado, así que digo... ¡Buenos días!
despedida("¡Hasta luego!") // Ya me voy, de modo que... ¡Hasta luego!

Hasta aquí todo normal. Pero ahora, imagina que queremos registrar a qué hora saludamos y nos despedimos. Podemos modificar las dos funciones para hacerlo, pero nos estaremos repitiendo.

En su lugar, podemos usar una sola función decoradora, que también puede ser denominada función de orden superior (recoge una función, devuelve una función).

// funciones originales
const bienvenida = (mensaje) => {
  console.log(`Ya he llegado, así que digo... ${mensaje}`)
}

const despedida = (mensaje) => {
  console.log(`Ya me voy, de modo que... ${mensaje}`)
}

// decorador
// el parámetro saludo es una función
const saludoDecorado = (saludo) => {
  return (mensaje) => {
    console.log(`Mensaje a las ${new Date().toLocaleString()}`)

    // llamada a la función entrante
    saludo(mensaje)
  }
}

// con el mismo decorador añadimos la funcionalidad a las dos funciones originales
const bienvenidaDecorada = saludoDecorado(bienvenida)
const despedidaDecorada = saludoDecorado(despedida)

bienvenidaDecorada("¡Buenos días!")
/* 
Mensaje a las 15/4/2022, 08:00:00 
Ya he llegado, así que digo... ¡Buenos días!
*/
despedidaDecorada("¡Hasta luego!")
/* 
Mensaje a las 15/4/2022, 17:00:00
Ya me voy, de modo que... ¡Hasta luego! 
*/

Vamos a desgranar lo que está pasando aquí. En primer lugar, tenemos las dos funciones originales a las que queremos añadirles una funcionalidad: registrar el momento en que muestran el mensaje.

Para hacer esto, definimos una nueva función que hará de decorador. La llamamos saludoDecorado y como parámetro de entrada recibe otra función. Lo que hace el decorador, es devolver a su vez otra función, que muestra el registro del tiempo y luego ejecuta a la función de entrada que tiene el propio decorador.

Finalmente, usamos el decorador para guardar la función que devuelve en dos variables diferentes: bienvenidaDecorada y despedidaDecorada. Como parámetro indicamos las dos funciones originales que queremos decorar. Luego ya podemos usar las funciones decoradas que extienden la funcionalidad de las funciones originales.

Decorando classes y sus métodos

Con las clases podemos hacer algo similar, aunque requiere que tengamos claro cómo funciona this en JavaScript. Debido a que es un poco más complejo, tenemos una propuesta de decoradores que eventualmente se añadirá al lenguaje.

Usando el mismo ejemplo anterior, tendríamos esta función decoradora:

// decorador
const saludoDecorado = (saludo) => {
  return (mensaje) => {
    console.log(`Mensaje a las ${new Date().toLocaleString()}`)
    saludo(mensaje)
  }
}

Ahora, creamos una clase Persona que tendrá métodos para dar la bienvenida y para despedirse:

// decorador
const saludoDecorado = (saludo) => {
  return (mensaje) => {
    console.log(`Mensaje a las ${new Date().toLocaleString()}`)
    saludo(mensaje)
  }
}

// clase
class Persona {
  constructor(nombre) {
    this.nombre = nombre
  }

  bienvenida(mensaje) {
    return console.log(`${this.nombre} llegado, así que digo... ${mensaje}`)
  }

  despedida(mensaje) {
    return console.log(`${this.nombre} se va, de modo que... ${mensaje}`)
  }
}

// decoramos los saludos de la clase
const yoMismo = new Persona("Pol")

// importante notar el uso del método bind()
const bienvenidaDecorada = saludoDecorado(yoMismo.bienvenida.bind(yoMismo))
const despedidaDecorada = saludoDecorado(yoMismo.despedida.bind(yoMismo))

bienvenidaDecorada("¡Hola!")
/* 
Mensaje a las 15/4/2022, 08:00:00 
Pol llegado, así que digo... ¡Hola! 
*/
despedidaDecorada("¡Adiós!")
/* 
Mensaje a las 15/4/2022, 17:00:00
Pol se va, de modo que... ¡Adiós! 
*/

Como puedes ver es posible decorar los métodos de una clase, pero de una forma un poco rara. Nos vemos obligados a usar el método bind() para gestionar correctamente this dentro del método de la clase. Si lo quitas, obtendrías este error:

TypeError: Cannot read properties of undefined (reading 'nombre')

Es necesario entender bien cómo funciona el contexto en JavaScript para evitar estos errores.

Propuesta de decoradores en JavaScript

Con la propuesta de decoradores en JavaScript (en etapa 2 desde marzo de 2022), podremos usar los decoradores de un modo similar a otros lenguajes, sin hacer cosas raras con el contexto.

Por ejemplo, el código anterior con clases quedaría de un modo similar a este:

// decorador
const saludoDecorado = (saludo) => {
  return (mensaje) => {
    console.log(`Mensaje a las ${new Date().toLocaleString()}`)
    saludo(mensaje)
  }
}

// clase
class Persona {
  constructor(nombre) {
    this.nombre = nombre
  }

  // uso del decorador
  @saludoDecorado
  bienvenida(mensaje) {
    return console.log(`${this.nombre} llegado, así que digo... ${mensaje}`)
  }

  // uso del decorador
  @saludoDecorado
  despedida(mensaje) {
    return console.log(`${this.nombre} se va, de modo que... ${mensaje}`)
  }
}

const yoMismo = new Persona("Pol")

yoMismo.bienvenida("Hola")
/* 
Mensaje a las 15/4/2022, 08:00:00 
Pol llegado, así que digo... ¡Hola! 
*/

Como puedes ver, de esta forma ya podemos usar el método de la clase después de instanciarla, el método ya lo tendremos decorado previamente.

Con esta propuesta también es posible decorar la propia clase, a parte de sus propiedades y métodos.

¿Merece la pena?

En mi opinión, desde luego que sí, tanto el uso de decoradores en general como la propuesta para que se usen de forma similar a otros lenguajes merece mucho la pena.

En primer lugar, el uso de decoradores nos permiten seguir el principio DRY, de las siglas en inglés de Don't Repeat Yourself. Podemos añadir información a las clases de una forma transparente y que beneficia el mantenimiento futuro del código, además de hacerlo más comprensible.

En segundo lugar, igualar este mecanismo a otros lenguajes hace que sea más fácil para los desarrolladores entender cómo se hace en JavaScript y reducir la barrera de entrada. Recuerdo la primera vez que vi cómo funcionaban las clases en JavaScript y su gestión del this. Teniendo experiencia previa con C++, Java y PHP no fue nada agradable, la verdad.

Estos cambios sin duda harán que a menos desarrolladores les pase esto, así que bienvenidos sean.