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

18 Abr 2011

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 objeto return, 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íamos undefined. 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 la expansión, vemos que hemos vuelto a definir el módulo utilizando var. Esta 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.

Más:

{8} Comentarios.

  1. guzman

    esta parte

    }( myApp || {} ));
    

    no me gusta, es lo tipico que se pone y que cuando lo ve alguien que no sabe lo que es… problemon

    creo que quedaria mas sencillo para novatos usar dentro el modulo

    // si modulo no esta definido se utiliza un objeto vacio 
          if(!module){
            module ={}
          }
    

    mi propuesta quedaria algo asi:

    // incluir el metodo bar a myApp 
      var myApp = ( function( module ){
    
     // si modulo no esta definido se utiliza un objeto vacio 
          if(!module){
            module ={}
          }
    
    // definir el mensaje 
      var bar = 'Goodbye Lenin';
     
    //crear la funcion para añadir el mensaje a la consola
      module.bar = function(){
        return bar;
      };
    
     // retornar el objeto global para poder seguir añadiendo cosas en cadena
      return module;
     
    }( myApp ));
    
    • Carlos Benítez

      No estoy de acuerdo en desoptimizar un código pensando en el nivel de quien retomará el trabajo. Si siempre actuaramos así, los códigos no avanzarían, sin existirían los patrones de diseño; lo ideal es que si alguien no sabe qué significa determinado argumento, lo investigue por su cuenta y continúe aplicando la misma dinámica.

      De todos modos, es una opinión personal 🙂
      Saludos!

  2. guzman

    ¿no hay un patron de diseño que se llama sanitice o algo asi?
    creo recordar que basicamente indicaba que no se debe de asumir que los parametros que se reciben en una funcion, modulo o constructor estan bien formados, de forma que lo primero que hay que hacer es «limpiarlos».

    si bien para reducir la especificidad de las clases y que estas hagan «solo» lo que deben hacer, se puede utilizar helper para este tipo de funciones, igual que se delega la gestion de logs a clases especificas, seria un caso parecido a lo que se hace con lo4j en java por ejemplo.

    var probando = funcion(objetoParametros){
    // me quito problemas,lalalala
    objetoParametros= sanitice(objetoParametros);


    }

    bueno que me lio, al final depende de con quien trabajes, si estas en una charcuteria y sabes que el siguiente que vea tu codigo se va a cargar todo lo que no entienda en cuanto lo vea.. pues coges ese tipo de manias 8)

    bien o mal dejemos que los demas users opinen, y el que gane paga las gominolas

    • Carlos Benítez

      Buenas;
      no tenemos un patrón de diseño del tipo ‘sanitize’ porque, por definición, un patrón de diseño debería ser una solución compacta a un problema especifíco. Si desconocemos el tipo de datos que esperamos en un objeto, no podemos crear una función genérica que lo valide.

      Para estos casos, contamos de nuevo con la coerción de datos y con la aplicación de valores por defecto mediante la técnica de comprobación booleana.

      En una función, tendríamos lo siguiente:

      function( param1, param2 /* , etc... */){
        param1 || ( param1 = ' blue' );
        param2 || ( param2 = {} );
        // etc...
      }
      

      Este tipo de asignación es extensible a los tipos y podríamos coercionar según interese:

      function( number1, number2 /* , etc... */){
        number1 = ( typeof( number1 ) == "number" ) ? number1 : 0;
        number2 = ( typeof( number2 ) == "number" ) ? number2 : 0;
        // etc...
      }
      

      De esta forma, podemos limpiar los datos antes de utilizarlos con cierta garantía.
      Saludos!

  3. jose

    ¿Y de esta manera puedo por ejemplo coger los métodos de un objeto String o Date y añadirle los que me interesen o modificar los que ya están creados?

    • Carlos Benítez

      Para el caso de los objetos preconstruídos en Javascript, como el String o Date, es más interesante actuar directamente sobre su prototipo:

      String.prototype.trim = function(){ return this.replace(/^\s+|\s+$/g,''); }
      

      Este método resulta más limpio que utilizar los submódulos cuando, realmente, no los hay.
      Saludos!

  4. Hector Zarco

    Carlos e visto varias veces que comentas como una desventaja el tema de utilizar los objetos con prefijos:

    myApp.sub.subFoo();

    Que tiene de malo esta practica? saludos!

    • Carlos Benítez

      Realmente, el único inconveniente es estético: llamar a un método teniendo que remontarse hacia atrás toda su cadena prototípica no es demasiado elegante y puede resultar redundante.

      Al margen de eso, realmente no existe ningún problema en cuanto a rendimiento o similar.

      Saludos.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *