¡Hola de nuevo!
Me he pasado toda la tarde del martes preparando esta entrada, pensando que iba a ser coser y cantar y ha resultado ser un horror debido en parte ciertos aspectos de JavaScript que hacen las cosas pesadillescas. Eso sí, me ha dado para una segunda entrada la mar de interesante que veréis dentro de poco.
Como os adelantaba en la entrada de metaprogramación en Python, aquí va la adaptación de la misma a JavaScript. Si queréis profundizar sobre metaprogramación podéis leer la entrada anterior y echarle un vistazo al material que colgué allí. Como entonces, es necesario un nivel intermedio de conocimientos teniendo claro el modelo de datos de JavaScript. De hecho, me vais a permitir advertiros que si cosas como…
- La cadena de prototipos
- Las funciones constructoras
- El hecho de que el objeto this sea dependiente de contexto en el que es utilizado
…os suenan a chino, leais antes esta estupenda introducción a JavaScript en la Mozilla Development Network o no entederéis na de na.
Como ocurría en el ejemplo de Python, nuestro objetivo será el de crear una clase de utilidad para obtener diversos recursos de una compañía con sitio web mycopany.com que publica su contenido en /projects, /customers y /employees. Una primera aproximación podría ser:
function MyCompany() { this.url = 'mycompany.com'; }; MyCompany.prototype.projects = function() { return this._get('/projects'); } MyCompany.prototype.employeecs = function() { return this._get('/employees'); } MyCompany.prototype.customers = function() { return this._get('/customers'); } MyCompany.prototype._get = function(path) { return 'GET ' + this.url + path; }
Como veis, los métodos tienen todos prácticamente la misma forma, la técnica del copy & paste nos permite añadir cuantos métodos nuevos necesitemos pero todos sabemos quién carga el copy & paste. Como decía en la última entrada, el objetivo de la programación dinámica es:
DRY
Don’t Repeat Yourself
Así que vamos a ver cómo expresar lo mismo sin escribir tanto:
function MyCompany() { this.url = 'mycompany.com'; }; function makeGet(path) { return function(){ return 'GET ' + this.url + '/' + path; }; } ['projects', 'employees', 'customers'].forEach(function(name){ MyCompany.prototype[name] = makeGet(name); });
La función makeGet devuelve una función con el comportamiento deseado, derivado del nombre del método. Si ahora a la compañía le diera por cambiar los recursos a /items/projects, /items/employees e /items/customers, bastaría modificar la función anónima devuelta por makeGet() para actualizar el comportamiento.
Lo que hemos hecho es sencillamente construir una lista con los nombres de los métodos que nos interesan, por cada uno de ellos llamamos a makeGet() para obtener la función adecuada y la añadimos al prototipo de la función MyCompany bajo el campo indicado por name para que quede a disposición de todas las instancias creadas por dicha función.
Podéis usar la shell d8 del motor V8 de JavaScript, node.js o, simplemente, Firebug en algún navegador moderno para probar los ejemplos:
d8> load("mycompany2.js") d8> c = new MyCompany(); [object Object] d8> c.employees() GET mycompany.com/employees d8> c.customers() GET mycompany.com/customers d8> c.projects() GET mycompany.com/projects
Bien, esto es bueno, tenemos una forma de mantener el código y de extender la funcionalidad fácilmente pero la solución está demasiado acoplada al problema. Veamos cómo la herencia puede ayudarnos:
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');
No muy claro, ¿verdad? ¡bienvenidos a JavaScript! Veamos qué ocurre aquí:
Lo primero que hemos hecho es crear una función constructora HTTP() cuyo prototipo contiene el método get(). Éste toma uno o más parámetros con los nombres de los métodos que se quieren crear y se encarga de llamar a makeGet() para construirlos. Para ello recorre los argumentos utilizando la variable especial arguments que parece un array. Si en vez de parecerlo, fuera un array, podríamos hacer arguments.forEach() y reproducir el comportamiento de hace un par de listados, pero como sólo lo parece, tendremos que aplicar explícitamente la función forEach() sobre arguments mediante call().
function f() { arguments.forEach(function(item) { return item; }); }; f(); TypeError: Object #<Object> has no method 'forEach'
Aclarado cómo recorrer los nombres de los métodos, veamos qué ocurre por cada uno de ellos. Lo que hacemos es añadir al objeto al que apuntará this, el campo indicado en name y como valor la función construída con makeGet(). ¿Qué vale exactamente this? Bueno, pues vale lo que hayamos indicado como tercer parámetro del método forEach() que es precisamente… this también. Sí, esto es así de cierto pero ahora en serio, ¿a qué apuntará this? Pues apuntará al objeto desde el que se llame la función get(), es decir, apuntará al prototipo de MyCompany y por tanto estaremos añadiendo los métodos al prototipo de MyCompany tal y como ocurría anteriormente.
Ahora todo está modularizado: la función HTTP() puede dejarse en un fihcero http.js mientras que las compañías pueden situarse en otro companies.js (así lo encontraréis en el repositorio de github). Ambas compañías heredan (a la manera de JavaScript) de HTTP() por lo que sus prototipos son instancias de HTTP(). Probemos el código:
d8> load("http.js") d8> load("companies.js") d8> mine = new MyCompany(); [object Object] d8> yours = new YourCompany(); [object Object] d8> mine.projects(); GET mycompany.com/projects d8> mine.employees(); GET mycompany.com/employees d8> mine.customers(); GET mycompany.com/customers d8> yours.projects(); GET yourcompany.com/projects d8> yours.employees(); GET yourcompany.com/employees d8> yours.customers(); (d8):1: TypeError: Object #<HTTP> has no method 'customers'
Bien, esto se comporta tal y como lo hacía el ejemplo de Python y el de Ruby aunque debo admitirlo: JavaScript no es precisamente amigable a la hora de metaprogramar. De hecho, JavaScript no es un lenguaje amigable pero posee un modelo de datos tan regular y versátil que otorga a los desarrolladores toda la libertad que necesitan. Como dirían los ingleses:
JavaScript gives the developers enough rope to hang themselves with if they want to
Ahora bien, cerrábamos la entrada anterior sobre Python mostrando una técnica en la que proporcionábamos los métodos sobre la marcha. Ni siquiera eran creados explícitamente llamando a get() sino que la clase los proporcionaba conforme se le iban pidiendo. Os voy adelantando que en JavaScript esto puede hacerse, y creo que la técnica me ha quedado tan bien, que le voy a dedicar una entrada completa y a mostrároslo en breve.
Tenéis todo el código de los ejemplos en:
https://github.com/lodr/metaprogramming
Dudas y opiniones, ¡a los comentarios! Espero que haya sido entretenido.