Decoradores y caché transparente
JavaScript ofrece una gran flexibilidad en cuanto a funciones. Puedes pasarlas como objetos y modificar su comportamiento de varias maneras, como veremos a continuación.
Caché de resultados
Imagina una función slow(x)
que realiza operaciones intensivas en la CPU pero siempre devuelve el mismo resultado para un mismo x
.
Si esta función se llama frecuentemente con los mismos argumentos, sería eficiente almacenar en caché los resultados para evitar cálculos repetidos.
En lugar de modificar directamente slow()
, podemos crear un «decorador» que agregue la funcionalidad de caché. Esto tiene sus ventajas, como veremos.
Aquí tienes el código con una explicación detallada:
function slow(x) {
// Código intensivo en CPU
alert(`Llamada con ${x}`);
return x;
}
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func(x);
cache.set(x, result);
return result;
};
}
slow = cachingDecorator(slow);
alert( slow(1) ); // El resultado de slow(1) se almacena en caché y se devuelve
alert( "Otra vez: " + slow(1) ); // Se devuelve el resultado de la caché de slow(1)
alert( slow(2) ); // El resultado de slow(2) se almacena en caché y se devuelve
alert( "Otra vez: " + slow(2) ); // Se devuelve el resultado de la caché de slow(2)
En este ejemplo,
cachingDecorator
es un decorador: una función especial que toma otra función y la envuelve para modificar su comportamiento.La idea es que podemos aplicar
cachingDecorator
a cualquier función para agregarle la funcionalidad de caché. Esto es útil ya que podemos reutilizar el decorador con diferentes funciones y mantener el código más simple.Usando
func.call
para el contextoEl decorador de caché que hemos visto hasta ahora no funciona bien con métodos de objetos.
Por ejemplo, considera el siguiente código donde queremos cachear el método
worker.slow()
:
let worker = {
someMethod() {
return 1;
},
slow(x) {
alert("Llamada con " + x);
return x * this.someMethod(); // Error aquí: `this` no está definido
}
};
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func.call(this, x); // Solución: usar `func.call` para establecer correctamente `this`
cache.set(x, result);
return result;
};
}
worker.slow = cachingDecorator(worker.slow);
alert( worker.slow(2) ); // Funciona correctamente ahora
alert( worker.slow(2) ); // Se devuelve el resultado de la caché
En este caso, func.call(this, x)
asegura que la función original se llame con el contexto correcto (this
). Esto es esencial para que los métodos de objetos funcionen correctamente con decoradores.
Manejo de múltiples argumentos
Para manejar funciones con múltiples argumentos, necesitamos modificar nuestro decorador para que funcione con cualquier número de argumentos. Aquí tienes una versión mejorada:
let worker = {
slow(min, max) {
alert(`Llamada con ${min},${max}`);
return min + max; // Operación intensiva en CPU
}
};
function cachingDecorator(func) {
let cache = new Map();
return function() {
let key = [].join.call(arguments, ',');
if (cache.has(key)) {
return cache.get(key);
}
let result = func.apply(this, arguments);
cache.set(key, result);
return result;
};
}
worker.slow = cachingDecorator(worker.slow);
alert( worker.slow(3, 5) ); // Funciona correctamente
alert( "Otra vez: " + worker.slow(3, 5) ); // Se devuelve el resultado de la caché
En este ejemplo, func.apply(this, arguments)
se utiliza para pasar todos los argumentos y el contexto correcto (this
) a la función original dentro del decorador.
Resumen
Los decoradores en JavaScript permiten agregar funcionalidades adicionales a las funciones sin modificar su código interno. Esto los hace reutilizables y mantiene el código más modular y fácil de mantener.