Lazy Function Definition Pattern

Patrones de diseño Ésta técnica, permite explotar una de las mejores características de JavaScript, los closures on-demand, con la cual podemos crear un closure la primera vez que se invoca una función, dándonos la ventaja de crear un caché, crear un estado interno privado, reducir operaciones complejas, etc. 🙄 Pero así como tiene ventajas, éste patrón de diseño tiene desventajas que debemos conocer.

Una de las ventajas que se logra con ésta técnica, es que se puede mejorar el rendimiento de las funciones, cuando en ellas se realizan operaciones constantes que siempre arrojan el mismo resultado, ya que se crea un cache con el resultado de cada operación efectuada. Y la otra gran ventaja es que al ser on-demand, el closure y la caché sólo se crearán cuando se invoque por primera vez la función; a ésta técnica se le conoce como lazy load o lazy evaluation.

>:D Primero que todo vamos a repasar algunos conceptos:

CLOSURE

“Un closure es un tipo especial de objeto que combina dos cosas: una función, y el entorno en que se creó esa función. El entorno está compuesto por las variables locales que están dentro del scope en el momento en que el closure fue creado” — MDN. Es decir que una función definida dentro del closure recuerda el entorno donde se creó (lexical scope) y tiene acceso a las variables de ese entorno (variables libres).

Código closure
Closure scopes

⭐ Lea más detalles en el artículo: Closures en JavaScript

Lazy evaluation

Se conoce también como call-by-need y es una estrategia que retrasa la evaluación de una expresión hasta que su valor es requerido evitando realizar cálculos innecesarios. El corto-circuito en la evaluación de expresiones boleanas ocurre cuando el segundo argumento es ejecutado o evaluado sólo si el primer argumento no satisface el resultado esperado según el operador AND o OR.

    function test(nombre, edad) {
        //lazy evaluation para canVote()
        if (nombre && canVote(edad)) {
            //ejecuta alguna tarea compleja
        }
    }

    //lazy evaluation para getDefaultConfig()
    var config = inputConfing || getDefaultConfig();

Lazy initialization

O también inicialización tardía, es una técnica utilizada para retrasar la creación de un objeto, el cálculo de una operación, o algún otro proceso costoso hasta que sea necesitado la primera vez. La forma tradicional de lograr el objetivo, es mantener un flag que nos permita saber si el objeto es requerido.

    function foo() {

        //t es el flag que determina
        //si el objeto fue requerido
        if (foo.t) {
            return foo.t;
        }

        console.log("inicializar");
        foo.t = new Date();
        return foo.t;
    }

Memoization

Memoization es una técnica de optimización usada principalmente para acelerar programas computacionales, almacenando en un caché los resultados de costosos llamados a funciones, y retornando el resultado almacenado cada vez que se den los mismos parámetros de entrada.

“Es una técnica en la cual los resultados parciales son almacenados en un caché y luego pueden ser reusados sin tener que calcularlos de nuevo”

    // memoiza el resultado de una
    // función que no recibe argumentos
    var calcLength = function() {
      var length = ... // expensive computation
      calcLength = function() { return length };
      return length;
    }

    // memoiza una función, creando un cache
    // para los argumentos de entrada
    var memoize = function (fn) {
      var cache = {};
      return function() {
        var key = serialize(arguments);
        if (key in cache) return cache[key];
        return (cache[key] = fn.apply(this, arguments));
      }
    }

➡ Recomiendo leer el artículo One-Line JavaScript Memoization de Oliver Steele, el cual nos ayudará a comprender mejor este concepto.


Lazy Function Definition

Éste patrón de diseño es una técnica de optimización con la que se consigue mejorar el rendimiento de funciones, reduciendo el costo computacional al hacer que una parte del código sea ejecutada solo una vez.

💡 Éste patrón de diseño pretende dar solución a los siguientes escenarios:

  • la función requiere inicializar objetos on-demand.
  • la evaluación de una expresión siempre obtiene el mismo resultado.
  • la función una vez ejecutada, siempre retorna el mismo valor en las siguientes llamadas.

⭐ Para implementar Lazy Function Definition, se deben seguir los siguientes pasos:

  1. Inicializar. Evaluar y hacer una serie de cálculos que determinan el valor a retornar.
  2. Redefinirse. La función se sobrescribe a sí misma retornando el valor ya calculado.
  3. Autoinvocarse. La función se llama así misma para retornar el valor calculado después de sobrescribirse.
function foo() {

    //1. inicializar
    var compute = new Date(); //some expensive task

    //2. sobrescribirse
    foo = function() {
        return compute;
    };

    //3. autoinvocarse
    return foo();
}

El ejemplo anterior lo podemos lograr con un IIFE, sin embargo hay unas diferencias, veamos:

    var foo = (function() {
        var compute = new Date(); //some expensive task

        return function() {
            return compute;
        }
    })();

😮 La principal diferencia radica en que el IIFE es ejecutado inmediatamente, lo que significa que el objeto compute se crea sin importar si se va a utilizar o no, mientras que en el caso de Lazy Function Definition, el objeto compute se crea on-demand sólo la primera vez que se invoque la función foo(). Ésto sin duda es una mejora en el performance de la función.

Vamos a ver un ejemplo donde la evaluación de una expresión siempre obtiene el mismo resultado.

var getText = function (DOMNode) {
 
    //inicializar y redefinir condicionalmente
    getText =
        typeof DOMNode.innerText !== "undefined"
          ? function (DOMNode) {
                return DOMNode.innerText }
          : function (DOMNode) {
                return DOMNode.textContent };
 
    //autoinvocar
    return getText(DOMNode);
}

😮 En el ejemplo anterior se detecta una sola vez cuál es la propiedad del nodo DOM de la que podemos obtener el texto. Una vez evaluada, la función se sobrescribe con la propiedad detectada según el browser y finalmente se autoinvoca la función redefinida para retornar el texto. 💡 Ésta técnica es muy útil para detectar características del browser (feature-detection).

Lazy Function Definition

A tener en cuenta

La principal desventaja de este patrón de diseño, es que la función al ser redefinida pierde la transparencia referencial y deja de ser un objeto de primera clase.

👿 Ésto quiere decir que no se puede garantizar la referencia a una función, por ejemplo, si es asignada a otra variable, enviada como argumento a otra función, o asignada como propiedad de un objeto, ya que cuando se redefine la función, en realidad se crea un nuevo espacio en memoria que contiene una dirección diferente a la de la función original.

Consideremos el siguiente ejemplo:

function getMonth(n) {

  // incializa
  var MONTHS = [
    "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio",
    "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre",
  ];

  // redefine
  getMonth = function(n) {
    if (n  12) {
      throw new RangeError("Mes incorrecto");
    }
    return MONTHS[n - 1];
  };

  // autoinvoca
  console.log("incializa, redefine, autoinvoca");
  return getMonth(n);
}

// sólo ejecuta los tres pasos la primera llamada
getMonth(3); // "incializa, redefine, autoinvoca"
getMonth(9);

var mylib = {
  getMonth: getMonth
};

// siempre ejecuta todos los pasos
mylib.getMonth(1); // "incializa, redefine, autoinvoca"
mylib.getMonth(6); // "incializa, redefine, autoinvoca"

😕 Podemos apreciar que al ser asignada la función como propiedad de un objeto, ésta siempre ejecuta los tres pasos, lo que significa que con cada llamado a mylib.getMonth() se está creando una nueva función que luego se asigna a la variable getMonth, porque la propiedad mylib.getMonth siempre contiene la referencia hacia la función original.

Asignación de memoria

^^’ Para resolver este problema, tendríamos que cambiar la línea en donde se redefine la función getMonth = function(n) por algo así: mylib.getMonth = function(n), pero la función ya dependería de un contexto externo, además, en el paso 2 no se estaría redefiniendo así misma, sino que estaría redefiniendo otro objeto, y en el paso 3 no se podría autoinvocar, sino la función redefinida de otro objeto, y aún así seguiríamos teniendo un problema si la asignamos a otra variable u objeto, veamos:

var mylib = {};

function foo() {
    var d = new Date();
 
    // esto es un antipatrón
    mylib.foo = function() {
        return d;
    };
 
    console.log("inicializa");
    return mylib.foo();
}

mylib.foo = foo;
var bar = foo;
var xlib = Object.assign({}, mylib);

mylib.foo(); // "inicializa"
mylib.foo(); // llama a la función redefinida

bar(); // "inicializa"
bar(); // "inicializa"

xlib.foo(); // "inicializa"
xlib.foo(); // "inicializa"

😡 Lo peor del caso, es que cualquier asignación diferente de mylib.foo resultará en la creación de una nueva función, y ésta nueva función será reasignada a la propiedad mylib.foo. Ésto es un grave problema, no solo porque se pierde la transparencia referencial, sino porque representa una fuga de memoria (memory leak) ocasionada por la creación innecesaria de objetos con cada llamado a la función.


NOTA: No utilizar este patrón de diseño cuando queremos asignar una lazy-function a otra variable, como método de un objeto, o pasarla como callback a otra función! 👿


Function Override

Ésta técnica de la Programación Orientada a Objetos (OOP) nos permite sobrescribir un método, conservando el mismo nombre pero cambiando algo en la implementación original.

💡 Es muy similar a Lazy Function Definition, sin embargo la diferencia es que para function override no se hace un reemplazo de la función original, lo que significa que se conserva la transparencia referencial. La sobrescritura del método se hace un nivel arriba en la cadena de prototipos, a nivel de instancia, de modo que el método original en el prototipo nunca es alterado.

// "clase" constructor
function Geolocation (lat, lng) {
  this.latitude = lat;
  this.longitude = lng;
}

Geolocation.prototype.getNearPlaces = function() {
  var radius = 200;
  // se efectua algún cálculo costoso
  var locations = getLocations(this.latitude, this.longitude, radius);


  // sobrescribe el método a nivel de instancia
  this.getNearPlaces = function() {
    return locations;
  };

  return locations;
};

:mrgreen: Veamos otro ejemplo en donde redefinimos el método en la instancia del objeto, y una vez se cumpla cierta condición, restauramos el método original del prototipo.

function downloadInProgress() {
  alert("still downloading...");
}

System.prototype.download = function (file) {
  // sobrescribe el método a nivel de instancia
  this.download = downloadInProgress;
 
  requestDownload(file, {
    callback: function() {
      // cuando la descarga termine,
      // se elimina el método sobrescrito en la instancia,
      // restaurando el método original del prototipo
      delete this.download;
    }
  });
};

Conclusión

Lazy Function Definition es una técnica que nos permite retrasar la creación de objetos hasta cuando sean requeridos, permitiendo una ganancia en el rendimiento, y como la función es redefinida por una versión mas corta que accede a los objetos ya inicializados en el closure, entonces la función se convierte en una versión más rápida y óptima de sí misma.

Éste es un patrón de diseño que vale la pena trabajar; cada vez que pensemos en mejorar el desempeño de una función, podemos recurrir a Lazy Function Definition o mejor aún Memoization.

Hay muchas formas en las que se puede aplicar este patrón de diseño, lo importante es siempre tener en cuenta el contexto y los problemas que soluciona, sus limitaciones, y que como cualquier patrón de diseño, puede ser combinado con otros para construir soluciones más robustas.

➡ Como siempre y para finalizar, recomiendo el artículo: Implementing Memoization in JavaScript.

😎 Happy Coding!

Advertisements

3 thoughts on “Lazy Function Definition Pattern

Comentarios

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s