jueves, 26 de abril de 2012

Ampliando el Patrón Módulo Javascript: submódulos


Introducción

Hasta ahora, hemos visto cómo se implementaba el patrón módulo tanto para su estructura más ‘clásica‘ como para su considerada evolución ‘revelada‘.
Sin embargo, es hora de afrontar algunos de sus inconvenientes con el fin de ir mejorando su definición hasta conseguir el patrón más sólido.

Escalabilidad

Sin duda, cuando escribimos módulos siguiendo las pautas estudiadas, tenemos una serio problema en cuanto a su escalabilidad: el módulo exige que todo el contenido esté recogido dentro de un mismo archivo, un mismo bloque. Esto parece ir en contra de su propia denominación ya que no podemos subdividir un código en subapartados.
Cuando tratamos con aplicaciones complejas con varios cientos de miles de líneas, parece interesante la posibilidad de trabajar con varios archivos de funcionalidades reducidas que se agreguen según se requieran a la estructura principal. Veamos cómo trasladar este escenario a nuestro patrón.

Subdividiendo el módulo

Retomemos la estructura básica del módulo:
var myApp = ( function(){
  // Code goes here...
}());
Como vimos en su momento, se trata de una función autoejecutable que permite, a través del objetoreturn, enviar una serie de funciones (métodos) al contexto global.
Si jugamos con el hecho de que las funciones autoejecutables permiten parámetros de inicialización, podemos conseguir comportamientos interesantes. Pensemos por ejemplo en el siguiente código:
var myApp = ( function(){
  // Code goes here...
}( myApp ));
Si como argumento del módulo pasamos el mismo módulo, estamos consiguiendo una referencia directa a su contenido. Suena interesante; basta entonces con que recojamos el objeto para contar con todas sus propiedades:
var myApp = ( function( module ){
 
  var oldModule = module;
 
}( myApp ));
Al ejecutar el bloque anterior y no existir una referencia previa de nuestro módulo, obtendríamosundefined. Es lógico; pero la idea es que, si el módulo ha sido definido con anterioridad, se puede expandir pasando a otro módulo su referencia. Probémoslo:
var myApp = ( function(){
 
  var foo = 'Hello World';
  return{
   foo : function(){ return foo; }
  }
 
}());
 
var myApp = ( function( module ){
 
  var bar = 'Goodbye Lenin';
 
  module.bar = function(){
    return bar;
  };
 
  return module;
 
}( myApp ));
 
console.log( myApp.foo() ); // Hello World
console.log( myApp.bar() ); // Goodbye Lenin
Funciona como se espera. Si observamos la segunda parte del código, el bloque que corresponde a laexpansión, vemos que hemos vuelto a definir el módulo utilizando varEsta nueva declaración no es necesaria pero puede dar consistencia cuando trabajamos con muchos archivos y no sabemos si en el original ha sido declarado en tiempo de ejecución.
Hemos pasado a este segundo bloque el primer módulo como argumento y lo hemos ampliado como si de un objeto más se tratase. Finalmente, hemos devuelto con return el módulo ya modificado-ampliado.

Previniendo errores

Puede darse el caso de que el módulo de expansión cargue sin estar disponible el original. O simplemente puede tratarse de un submódulo con funcionalidad independiente que no debería requerir del general.
En estos casos, al utilizar la referencia de un módulo que no existe, dará un mensaje de error:
var myApp = ( function( module ){
 
  var bar = 'Goodbye Lenin';
 
  module.bar = function(){
    return bar;
  };
 
  return module;
 
}( myApp ));
 
console.log( myApp.bar() );
 
// TypeError: module is undefined
Para prevenir este tipo de errores y hacer la estructura más independiente, podemos recurrir a la sustitución de variables por el método booleano del mismo modo a como, por ejemplo, asignamos valores por defecto a los argumentos de una función:
var myApp = ( function( module ){
 
  var bar = 'Goodbye Lenin';
 
  module.bar = function(){
    return bar;
  };
 
  return module;
 
}( myApp || {} ));
 
console.log( myApp.bar() ); // Goodbye Lenin
Al pasar un objeto vacío en caso de que la referencia no exista, prevenimos errores por indefinición.

Sobreescribiendo métodos

Al igual que con este sistema podemos ampliar un módulo, reescribir sus métodos públicos en demanda resulta igual de sencillo:
var myApp = ( function( module ){
 
  var bar = 'Goodbye Lenin';
 
  module.foo = function(){
    return bar;
  };
 
  return module;
 
}( myApp || {} ));
 
console.log( myApp.foo() ); // Goodbye Lenin
Esto nos permite modificar el comportamiento de un módulo en tiempo de ejecución dependiendo de las dependencias que carguemos. La estructura así se vuelve mucho más flexible.

Submódulos reales: plugins

Si bien es interesante la posibilidad de ampliar módulos, hasta ahora hemos hablado de hacerlo directamente en su núcleo.
En las aplicaciones reales, es probable que, en lugar de modificar el propio core de un módulo, nos sintamos más cómodos creando submódulos (plugins) que añadan funcionalidad.
Para ello, partiendo de la idea anterior, crearíamos bloques similares al siguiente ejemplo:
myApp.sub = (function () {
 
  var fooBar = 'A submodule';
 
  return {
    subFoo : function(){
      return fooBar;
    }
  }
 
}());
 
console.log( myApp.sub.subFoo() ); // A submodule
Con esta forma de expansión, ganamos legibilidad y preservamos intacto el núcleo del módulo original. El único inconveniente es quizá tener que invocar los métodos del submódulo a través de su correspondiente prefijo. Al margen de este detalle, el resultado es muy limpio y transparente.

Conclusión

El patrón del módulo puede ser fácilmente escalado para cubrir los requisitos de grandes aplicaciones o simplemente para conseguir una mejor organización interna muy útil en equipos de desarrollo segmentados.
Y todo este comportamiento lo hemos conseguido utilizando conceptos básicos del lenguaje, sin mayores artificios o complejas librerías de terceros.

No hay comentarios:

Publicar un comentario