25 junio, 2009

Programación con singletons y árbitros de eventos

He traducido el más que interesante artículo de Aaron Newton, Singletons and Event Arbiters:

¿Cuál es la mejor forma de definir código, asignarlo a una variable global y conseguir que se ejecute todo a la vez?

Usamos MooTools en el servidor en mi código ASP por medio de JScript y necesitamos objetos singleton globales como el de inicio de sesión. Puesto que no hay una forma estándar clara de hacerlo, lo he intentado y he generado algo como esto:
var LoggerObject = function() {
  // código de inicio de sesión
} 
var Logger = new LoggerObject() 
Está bien, pero parece que me estoy dejando algo. ¿Cómo lo hacen los profesionales? ¿Cuáles son las ventajas y desventajas de otros métodos?
Para los que no estén familiarizados con el concepto, un singleton (traducido literalmente sería "hijo único") es una clase diseñada para tener sólo una instancia (o un número limitado de ellas). En JavaScript, cualquier objeto puede heredar de cualquier otro objeto y no se puede prevenir que esto pase, por lo que en el sentido más estricto de la expresión, no es posible tener un singleton en JavaScript.

MooTools ofrece una función llamada "Class" que nos ayuda a gestionar la herencia entre objetos. Crear un objeto que es una instancia de una clase se consigue con el operador new:
var myWidget = new Widget()
Si lo que se desea es crear una clase e inmediatamente convertirla en una instancia, es posible hacer los siguiente:
var myInstance = new new Class({...})
El doble new invoca una función devuelta por la clase y que devuelve un objeto (una instancia de esa clase). Raramente se puede encontrar una razón para hacer esto así. De hecho, la única razón que se me ocurre es si se quiere que esa clase extienda a otra:
var myInstance = new new Class({
    Extends: SomeOtherClass,
    // nuevas propiedades aquí
});
El problema con este esquema es que no consigue mucho. JavaScript trata sobre todo con objetos, y hay mejores formas de hacer esta tarea.

Yo uso objetos (como singletons) continuamente para mi código de aplicación (obviamente casi nunca para plugins, que se supone que deben extenderse e instanciarse). Uso objetos "site" para apuntar métodos para gestionar el estado (como por ejemplo si el usuario ha iniciado sesión, su nombre de usuario, etc). Simplemento hago un objeto JavaScript básico, así:
var mySite = {
    login: function(username){ this.username = username; },
    showWelcome: function(){
        $("welcome").set("html", "Welcome " + this.username);
    }
};
No hay nada especial aquí - JavaScript funciona así. Es importante fijarse que las clases en MooTools existen para darnos funcionalidad y dejarnos derivarla a través de la herencia. Si creo una clase llamada Widget y luego quiero crear una versión llamada Widget.Ajax, puedo extender Widget y añadir sólo las partes ajax. Con los singletons el tema está en que no vas a crear más que uno.

En cualquier caso, hay algunas otras cosas que la Class de MooTools nos ofrece, como mixins (clase que ofrece cierta funcionalidad para ser heredada por una subclase, pero no está ideada para ser autónoma). Ejemplos de esto serían Event y Option que ofrecen a las instancias métodos como setOptions o addEvent. En estos casos aún puedes usar la declaración de objeto de arriba y extenderlo:
var mySite = {
    login: function(username){
        this.username = username;
        this.fireEvent("login");
    },
    showWelcome: function(){
        $("welcome").set("html", "Welcome " + this.username);
    }
};
$extend(mySite, new Events());
Aunque generalmente yo hago justo de la otra forma porque creo que es un poco más limpio (aunque se consigue el mismo efecto):
var mySite = new Events();
$extend(mySite, {
    login: function(username){
        this.username = username;
        this.fireEvent("login");
    },
    showWelcome: function(){
        $("welcome").set("html", "Welcome " + this.username);
    }
});
Con esta forma se evita la necesidad del tosco patrón new new Class y resulta más legible en mi opinión.

Árbitros de eventos

Lo que me lleva a una práctica común que suelo usar cuando construyo aplicaciones. Cuando construyo un sitio o aplicación, intento hacer las cosas lo más modulares posible. Lo hago de forma que primero mi sitio funcione sin JavaScript, y entonces empiezo a añadir los elementos de la interfície con ajax y animaciones y tablas ordenables y todo esa fiesta. También hago que el JavaScript mismo sea modular. Todo lo que pueda ser una clase, hago que sea una clase. Lo que queda es el código que instancia esas clases.

Pero ¿qué pasa si el código que instancia un widget necesita información de otro widget? Por ejemplo, ¿qué hacer si nuestro widget welcome (bienvenida) necesita saber el estado del widget de inicio de sesión? Bueno, si nuestro widget de inicio de sesión almacena su estado en el objeto mySite, entonces el widget de welcome puede inspeccionarlo, ¿no? Eso está bien; si la página carga y mySite.username no está inicializado (undefined) entonces el widget de welcome sabe que el usuario no ha iniciado sesión. Todo correcto.

Pero ¿qué pasa cuando el estado cambia? ¿Qué pasa cuando el usuario inicia sesión? Ahora necesitamos un evento - onLogin o similar, pero eso significa que Welcome necesita apuntarse al widget de inicio de sesión y entonces hemos perdido algo de modularidad al introducir una dependencia. Welcome no puede funcionar sin el inicio de sesión. En este caso, no parece que se trate de una mala dependencia - después de todo, un mensaje de bienvenida sin una funcionalidad de inicio de sesión no tiene mucho sentido - pero podría haber docenas de otros widgets que necesiten hacer cosas cuando el usuario inicia sesión y podríamos queere que esos widgets funcionen incluso si el usuario no ha iniciado la sesión. Es más, podríamos querer cargar el widget de inicio de sesión bajo demanda - cuando el usuario intente iniciar sesión por ejemplo. No es posible hacer eso si todos nuestros widgets necesitan apuntar eventos al widget de inicio de sesión.

Esta es la razón por la que hago que mySite sea una instancia de Events. mySite realmente no tiene eventos nativos en sí mismo. En su lugar, otras clases le apuntan eventos y disparan eventos por ella. Así, en nuestro puzzle de arriba sobre nuestro widget de inicio de sesión, en lugar de que todos los widgets se apunten a un evento onLogin, pueden apuntarse a mySite, y entonces nuestro widget de inicio de sesión dispara ese evento pero no sobre sí mismo sino sobre mySite:
var mySite = new Events();
var Login = new Class({
    //...login logic and stuff
    loginSuccess: function(username){
        mySite.username = username;
        mySite.fireEvent("login");
    }
});
var Welcome = new Class({
    initialize: function(){
        this.showMessage();
        mySite.addEvent("login", this.showMessage.bind(this));
    },
    showMessage: function(){
        $("welcome").set("html", mySite.username ? "Welcome " + mySite.username : "Please log in");
    }
});
Ahora nuestras clases ya no dependen entre ellas. Todas dependen ahora de mySite pero eso ya pasaba antes. Ahora pueden venir y marcharse cuando quieran sin preocuparse por las demás. Este incremento de la modularidad se agradece cuando el sitio crece en complejidad. Algunas páginas podrían tener algunos widgets y algunos podrían tener otros. Puede apuntar la lógica al objeto mySite sin tener que lidiar realmente con esas dependencias.

Publicar un comentario en la entrada

Últimos links en indiza.com