Os decía en la última entrada que podíamos solucionar en JavaScript el problema de metaprogramación de generar los comandos sobre la marcha, en el momento mismo de ser invocados y no antes. Os decía también que creía que me había quedado muy bien y en honor a la verdad debo decir que la idea la saqué del artículo The Command Pattern in JavaScript de Peter Michaux, ¡pero sólo me había leído el primer párrafo!, en especial la parte en la que dice:
In JavaScript, building our own dot operator function is one way to make indirect object function property calls and escape the built-in paradigm.
En JavaScript, construir nuestro propia función operador punto es una manera de hacer llamadas a función de manera indirecta y así escapar del paradigma por defecto.
Así que me puse a implementar este operador «punto» y cuando hube terminado me di cuenta de lo parecido con la solución propuesta por Peter.
Antes de implementar nada merece la pena entender cómo JavaScript busca los atributos de un objeto: para empezar, busca en el objeto y si ahí no está, busca en su cadena de prototipos que es otro objeto. Si ahí tampoco está, busca en la cadena de prototipos de la cadena de prototipos y así, hasta alcanzar cierto objeto cuya cadena de prototipos es null. Entonces devuelve undefined. En Firefox y en node.js, podemos consultar la cadena de prototipos de un objeto mediante el atributo especial __proto__,
La manera que tiene JavaScript de establecer la cadena de prototipos de un objeto es mediante el operador new y la propiedad prototype presente en todos los objetos. Cuando hacemos var o = new F(), aparte de construir el objeto mediante F(), hacemos que la cadena de prototipos del mismo apunte al atributo F.prototype.
Veamos una interacción en node.js* asumiendo que hemos cargado el siguiente listado:
function HTTP() {}; HTTP.prototype.get = function() { function makeGet(path) { return function(){ return 'GET ' + this.url + '/' + path; }; } Array.prototype.forEach.call(arguments, function(name){ this[name] = makeGet(name); }, this); }; function MyCompany() { this.url = 'mycompany.com'; }; MyCompany.prototype = new HTTP(); MyCompany.prototype.get('projects', 'employees', 'customers'); function YourCompany() { this.url = 'yourcompany.com'; } YourCompany.prototype = new HTTP(); YourCompany.prototype.get('projects', 'employees');
> mine = new MyCompany(); { url: 'mycompany.com' } > mine.__proto__ { projects: [Function], employees: [Function], customers: [Function] } > mine.__proto__.__proto__ { get: [Function] } > mine.__proto__.__proto__.__proto__ {} > mine.__proto__.__proto__.__proto__.__proto__ null
* Si probáis esto mismo en Firefox, podéis llevaros la sorpresa de que el prototipo de mine contenga el método get(). Realmente esto no es así, lo que ocurre es que la consola de Firefox nos muestra todos los atributos de un objeto: los suyos y los disponibles a través de la cadena de prototipos. Podéis consultar si get() pertenece al objeto y no a su cadena con el método hasOwnProperty() que devolverá false para ‘get’ y true para ‘employees’, por ejemplo.
Ahora sí, hemos repasado todos los conceptos que nos hacían falta y parece, por tanto, que no hay forma de interceptar o manipular cómo se recuperan los atributos de un objeto… ¿o sí?
method_missing
Como el título de la entrada indica, la idea consiste en emular el comportamiento de method_missing de Ruby. Cuando Ruby busca un método en un objeto, busca en el propio objeto; sino está ahí, en su clase y si tampoco está, sube por la jerarquía de clases. Si aun así no se encuentra, se llama al método especial method_missing() y si este tampoco está, se lanza la excepción NoMethodError.
Voy a extender el prototipo de Object para que incluya un método similar cuya implementación por defecto consiste precisamente en lanzar una excepción con el mensaje ‘NoMethodError’. Si ya tenías cargados los listados anteriores puedes copiar y pegar los siguientes y añadirás los métodos nuevos sobre la marcha: piénsalo bien, añades los métodos en tiempo de ejecución. ¡Esa es la gracia!
Object.prototype.method_missing = function() { throw 'NoMethodError'; };
Como adelantábamos al comienzo de la entrada, construiremos nuestro operador ‘punto’. Una implementación podría ser:
Object.prototype.dot = function(name) { var args = Array.prototype.slice.call(arguments, 1); var f = this[name]; if (!f) { args.splice(0, 0, name); return this.method_missing.apply(this, args); } else { return f.apply(this, args); } };
Es interesante hacer notar que realmente, esto no sustituye al operador punto dado que no recupera un atributo sino que lo invoca así que sólo debería usarse para realizar llamadas a métodos. El nombre ‘dot’ es apropiado para recordar qué estamos haciendo pero resultaría más correcto que se llamase ‘send’ como ocurre en el artículo de Peter.
Podemos cargar los ejemplos anteriores y probarlos en node.js:
> yours = new YourCompany(); { url: 'yourcompany.com' } > yours.employees(); 'GET yourcompany.com/employees' > yours.dot('employees'); 'GET yourcompany.com/employees' > yours.customers(); TypeError: Object #<HTTP> has no method 'customers' > yours.dot('customers'); NoMethodError > yours.url 'yourcompany.com' > yours.dot('url') TypeError: Object yourcompany.com has no method 'apply'
Ahora es fácil, sencillamente reimplementaremos method_missing() en el prototipo de HTTP para que, si llega a invocarse, cree un nuevo método mediante get() y lo invoque. Vale la pena recalcar que method_missing() se llama de tal manera que this hace referencia al objeto para el que se trató de invocar el método:
HTTP.prototype.method_missing = function(name) { this.constructor.prototype.get(name); return this.dot.apply(this, arguments); };
Mediante this.constructor.prototype llegamos al prototipo del constructor que es precisamente el objeto al que apunta la cadena de prototipos del objeto. Contra lo que dicta el sentido común, esto no añade los nuevos métodos al prototipo
Ahora sí, tenemos una forma de crear métodos sobre la marcha sin preocuparnos de si existían antes o no y resuelto como lo haría Ruby. Además, las nuevas funciones extienden el prototipo de Object por lo que cualquier objeto dispondrá de ellas.
No discutiré más y con esto doy por terminadas las entradas sobre metaprogramación a la espera de vuestros comentarios y aportaciones. Espero que os haya sido tan entretenido como a mí.
Si os ha gustado la entrada, compartidla, por favor.
Edit 2012/04/09: He corregido el código de la función dot(), añadido al repositorio de github los ejemplos y he extendido la explicación sobre this.constructor.prototype dado que no era del todo correcta.
Además, creo que voy a hacer un último post con algo de discusión… no sé, ya veré.
Tenéis todo el código de los ejemplos en:
https://github.com/lodr/metaprogramming
Si os gusta la entrada, ¡comentadla y compartidla!