La programación modular y la orientada a objetos

Un módulo en programación podemos definirlo como un trozo de programa desarrollado de forma independiente, de tal forma que es esta independencia con respecto al resto del programa lo que lo caracteriza. La programación modular consiste en que cada módulo representa un elemento abstracto que debe ser usado desde fuera con sólo saber que hace pero sin necesidad de saber cómo lo hace. La independencia del módulo se incrementa si aplicamos ocultación, que consiste en que el que use el módulo solo tiene visible la información de la interfaz, que viene a ser una lista con las variables y métodos públicos disponibles y cómo usarlos. La ocultación no es principalmente un aspecto de seguridad, aunque también pudiera, sino de simplificar los programas mostrando sólo lo que se va a usar desde fuera.

Mediante la descomposición modular podemos dividir un programa muy complejo en diferentes módulos, obteniendo así ventajas como la facilidad para mantenerlos o la posibilidad de reutilizar el módulo en otros programas.

Un módulo tiene mayor independencia cuando tiene un menor acoplamiento y una mayor cohesión:

  • El acoplamiento es más fuerte cuando un módulo se apoya en exceso en otros para funcionar. Cuanto menos acoplamiento exista más independientes serán los módulos. Algún acoplamiento tendrá que existir pues no tiene utilidad un módulo completamente aislado, pero debe mantenerse en el mínimo para minimizar a su vez el esfuerzo en el mantenimiento o reutilización.
  • La cohesión de un módulo será más fuerte si hay una gran relación entre todos los elementos interiores del módulo. Digamos que el módulo agrupa todas las variables y funciones destinadas a caracterizar una determinada funcionalidad en el programa global.

La programación orientada a objetos (POO) es básicamente una programación modular que utiliza los conceptos de abstracción, ocultamiento, acoplamiento y cohesión. Además incopora algunos muy potentes como el encapsulamiento, la herencia y el polimorfismo. A veces se confuden los términos ocultamiento y encapsulamiento. El encapsulamiento usa las técnicas de la ocultación para aislar el objeto del exterior. Este aislamiento hace que los datos (propiedades) del objeto sólo pueden gestionarse con los operaciones (métodos) definidos en ese objeto. Se decía que JavaScript no podía ocultar y/o encapsular datos porque los objetos no podían tener variables y métodos privados como sucede con Java y otros lenguajes. Douglas Crockford fue uno de los que echó abajo este mito tal como expone en el documento del año 2001 Private members in JavaScript, lo cual se consigue precisamente con el efecto closure. Por otro lado JavaScript no tiene clases como Java por ejemplo, pero se simulan con funciones que hacen las veces de constructores.

La flexibilidad de JavaScript hace que se utilice generalmente como una mezcla de programación modular y POO. En general un módulo en JavaScript suele ubicarse en un archivo .js externo a la página. A veces hablamos de módulo independientemente de que sea programado en POO o simplemente a modo de programación modular (colección de variables y funciones estrechamente relacionadas entre sí). Incluso un módulo puede contener una combinación de ambos paradigmas, como también lo puede ser la colección de todos los módulos de una página. Esta flexibilidad de JavaScript da lugar a una enorme variedad de formas de programar y a tener que aprender y usar técnicas para simular los conceptos de programación que necesitamos. Por ejemplo lo expuesto por Douglas Crockford es un artilugio para lograr el propósito del ocultamiento. Estas técnicas son implementadas gradualmente por la comunidad de programadores y hay quiénes se encargan de recopilarlas, llámandolas Patrones de diseño JavaScript (JavaScript Design Patterns). El libro Learning JavaScript Design Patterns de Addy Osmani define un patrón JS como una solución reutilizable que puede ser aplicada a problemas comunes en aplicaciones web con JavaScript. El sitio JavaScript Patterns Collection de Shi Chuan recopila patrones de diversas fuentes, mostrando también una lista de enlaces de referencia de donde fueron obtenidas.

La ventaja de usar estos patrones es que son soluciones ampliamente probadas. Pero no debemos "copiar y pegar" sin más. Sin son soluciones hemos de entender qué problemas resuelven. En primer lugar tenemos que identificar el problema de programación. Luego nos preguntaremos si ya existe una patrón para eso. Usar patrones nos asegura que es la solución óptima pues nos estamos beneficiando de la experiencia de otros programadores.

En los siguientes apartados veremos sólo algunos ejemplos de patrones para mostrar como el efecto closure nos permite fijar el comportamiento del módulo. Un ejemplo más lo expondré en el siguiente tema para tratar de resolver el problema del espacio de nombres global.

Patrón módulo

El patrón módulo trata de encapsular las variables de tal manera que las diferenciamos entre privadas y públicas. Con esto conseguimos el efecto de ocultar lo privado al exterior, no tanto por motivos de seguridad sino para aclarar la vista o perspectiva que desde el exterior del módulo se tiene sobre los datos y métodos del mismo. Veámos este ejemplo:

Ejemplo:

<div class="ejemplo-linea">
    <div><input type="button" class="codigo"
        value="alert(modulo.variablePublica)"
        onclick="alert(modulo.variablePublica)" />
    </div>
    <div><input type="button" class="codigo"
        value="modulo.metodoPublico(Date())"
        onclick="modulo.metodoPublico(Date())" />
    </div>
</div>
<script>
    var modulo = (function(){
        //Variables y métodos privados
        var variablePrivada = "Esto es privado";
        var metodoPrivado = function(argumentoPrivado){
            alert(variablePrivada + " Fecha: " + argumentoPrivado);
        };
        //Devuelve un objeto con lo público
        return {
            variablePublica: "Esto es público",
            metodoPublico: function(argumentoPublico){
                variablePrivada += " (ahora es público a través del método público)";
                metodoPrivado(argumentoPublico);
            }
        };
    })();
</script>

El patrón módulo
Patrón JavaScript MóduloImagen no disponible
El módulo está ubicado en window, el espacio global y sólo ofrece las variables y métodos públicos.

En esta primera captura de pantalla de la herramienta Firebug de Firefox puede ver como al declarar var modulo pasa a ubicarse en el espacio global window. Sólo expone la variable y el método público. Con el primer botón del ejemplo accedemos a la variable pública. Las variables y métodos privados quedan ocultos por el alcance y no pueden verse desde el exterior. Aún así podemos trabajar con estos datos privados por medio de los métodos públicos. Con el segundo botón ejecutamos el método público. Éste accede a la variable privada, modificándola y llamando también al método privado.

Vea que el módulo es una función auto-ejecutable. Cuando la página llega a ese script además de cargarlo lo ejecuta, devolviendo el objeto interior. Como la función metodoPublico() hace referencia a las variables locales, estas son variables libres y por tanto quedan atadas al closure de ese método público. Vea la segunda captura de pantalla obtenida con la herramienta de desarrollo de Chrome poniendo un punto de interrupción en el código. En el alcance de la función (function scope) tenemos acceso a las variables y funciones privadas ubicadas dentro del closure.

Patrón singleton

El patrón singleton nos permite ejecutar una única instancia de un módulo. En la primera ejecución declara una variable instancia que devolverá con la ejecución de una función iniciar() el cuerpo del módulo, es decir, las variables y métodos privados en el closure y un objeto con las variables y métodos públicos.

Ejemplo:

La función pública no estará disponible hasta que instanciemos el singleton:
Si ahora instanciamos el singleton podemos pulsar el botón anterior y accedemos a la función pública
Cualquier otra instanciación conduce al mismo singleton
El cuerpo del módulo de todos los singletones es el mismo. La función pública accede a las mismas variables. Observe que la fecha-hora es la misma, momento en que se inició el primer singleton.
Podemos cerciorarnos de que es el mismo singleton
<div class="ejemplo-linea">
    <div>La función pública no estará disponible hasta que instanciemos el singleton:</div>
        <input type="button" class="codigo"
        value="ejecutarFuncionPublica(unSingleton)"
        onclick="ejecutarFuncionPublica(unSingleton)" />
    <div>Si ahora instanciamos el singleton podemos pulsar el botón anterior y accedemos
    a la función pública</div>
        <input type="button" class="codigo"
        value="unSingleton = singleton.obtenerInstancia()"
        onclick="unSingleton = singleton.obtenerInstancia()" />
    <div>Cualquier otra instanciación conduce al mismo singleton</div>
        <input type="button" class="codigo"
        value="otroSingleton = singleton.obtenerInstancia()"
        onclick="otroSingleton = singleton.obtenerInstancia()" />
    <div>El cuerpo del módulo de todos los singletones es el mismo. La función pública
    accede a las mismas variables. Observe que la fecha-hora es la misma, momento
    en que se inició el primer singleton.</div>
        <input type="button" class="codigo"
        value="ejecutarFuncionPublica(otroSingleton)"
        onclick="ejecutarFuncionPublica(otroSingleton)" />
    <div>Podemos cerciorarnos de que es el mismo singleton</div>
        <input type="button" class="codigo"
        value="sonIguales(unSingleton, otroSingleton)"
        onclick="sonIguales(unSingleton, otroSingleton)" />
</div>
<script>
    var singleton = (function(){
        //Una instancia que haremos única
        var instancia;
        //iniciar el singleton: Esto devolverá el cuerpo del módulo,
        //lo privado en el closure y lo público en un objeto devuelto
        function iniciar(){
            var varPrivada = "Esto es privado. Fecha: " + Date();
            var funPrivada = function(){
                //hace algo
            };
            return {
                varPublica: "Esto es público",
                funPublica: function(){
                    alert(this.varPublica + " y " + varPrivada);
                }
            };
        }
        //En la autoejecución devolvemos una función que inicia
        //el singleton en todas las declaraciones
        return {
            obtenerInstancia: function(){
                if (!instancia){
                    instancia = iniciar();
                    alert("Singleton instanciado");
                } else {
                    alert("Este singleton ya fue instanciado");
                }
                return instancia;
            }
        };
    })();
    //Declaramos dos variables del singleton
    var unSingleton;
    var otroSingleton;
    //Funciones auxiliares para probar el ejemplo
    function ejecutarFuncionPublica(esteSingleton){
        //Si el singleton no ha sido instanciado no llamamos a la
        //función pública para evitar un error
        if (!esteSingleton){
            alert("El singleton no ha sido instanciado");
        } else {
            esteSingleton.funPublica();
        }
    }
    function sonIguales(uno, otro){
        //Sólo comparamos si hemos instanciado los dos singletones
        if (uno && otro){
            return alert(uno === otro);
        } else {
            alert("Instancie los dos singletones y luego compruebe esto");
        }
    }
</script>

Patrón Singleton Esta captura de pantalla la he obtenido con la herramienta de Chrome tras poner un punto de interrupción al salir de la función auxiliar sonIguales() que he puesto para probar el ejemplo. Previamente habremos instanciado unSingleton y otroSingleton con los botones del ejemplo. Se observa en la imagen que el objeto singleton tiene la función obtenerInstancia que en todos las casos nos retorna la variable instancia que apunta al objeto que está en el closure de esa función. Es decir, instancia es una variable libre para la función obtenerInstancia y por tanto JavaScript la mantendrá viva desde el momento que la estamos devolviendo al exterior. Bien con la función iniciar() la primera vez o bien devolviendo la propia variable en las sucesivas instanciaciones.

Se observa como nuevamente el efecto closure está en medio de estos patrones. Este ejemplo, que aparece en el libro de Addy Osmani, en el apartado The Singleton Pattern, es una muestra de como los patrones diseñados por otras personas pueden ayudarnos a "domar" JavaScript, un lenguaje como dije más arriba que por su flexibilidad puede darnos un montón de problemas.

Patrón lanzador o inicializador

En mi sitio tengo algunos módulos cuyo propósito es construir dinámicamente elementos HTML en la página. Al mismo tiempo ofrecen algunas funciones públicas que exponen variables privadas o ejecutan funciones privadas, con el objeto de gestionar el comportamiento de los elementos HTML construidos. La idea básica es generar un contenido HTML con JavaScript que pueda reutilizarse en diferentes páginas. Veámos este ejemplo:

Ejemplo:

<div class="ejemplo-linea">
    <div id="lanza-aqui"></div>
    <input type="button" class="codigo"
        value="unLanzador.verVariablePrivada()"
        onclick="unLanzador.verVariablePrivada()" />
    <input type="button" class="codigo"
        value="ejecutaOtroLanzador()"
        onclick="ejecutaOtroLanzador()" />
</div>
<script>
    var lanzador = (function(){
        //variables privadas
        var iniciado = false;
        var variablePrivada = "Esto es privado";
        var funcionPrivada = function(){
            alert(Date());
        };
        //En la auto-ejecución devuelve esta función lanzadora o iniciadora
        //del módulo, construyendo el HTML si no ha sido iniciado.
        return function(id, argumento){
            if (iniciado){
                alert("Lanzador ya fue iniciado. Sólo puede ejecutarse una vez.");
            } else {
                //Construye el HTML
                var donde = document.getElementById(id);
                var elemento = document.createElement("div");
                elemento.innerHTML = argumento;
                elemento.style.border = "blue solid 1px";
                elemento.onclick = funcionPrivada;
                donde.appendChild(elemento);
                iniciado = true;
                //Devuelve un objeto público
                return {
                    verVariablePrivada: function(){
                        alert(variablePrivada);
                    }
                };
            }
        };

    })();
    //
    var unLanzador = lanzador("lanza-aqui", "Contenedor generado por el lanzador. " +
                              "Haz click aquí para ver la fecha");
    var otroLanzador;
    function ejecutaOtroLanzador(){
        otroLanzador = lanzador("lanza-aqui", "Haz click aquí TAMBIÉN");
    }
</script>

El patrón lanzador
Patrón JavaScript LanzadorImagen no disponible

En la primera captura de esta pantalla podemos ver como queda la función lanzador después de la auto-ejecución cuando se carga el script. En el closure se fijan las variables y funciones privadas, pues al devolver la función lanzadora o iniciadora esas son variables libres que se blindan. Cuando ejecutamos la función lanzadora con unLanzador = lanzador(...) se almacena unLanzador como un objeto con el método público devuelto verVariablePrivada(), como se observa en la segunda imagen. Dispone del closure con acceso a las variables privadas.

Separar la acción de lanzar o iniciar una construcción dinámica de HTML del momento en que se carga el módulo es necesario cuando debemos considerar si ya han sido cargados otros elementos de la página. En este ejemplo sólo podemos construir el HTML si ya existe el elemento con id="lanza-aqui". En una situación más compleja habría que lanzar la construcción del HTML desde el window.onload, pues podría tener que acceder a otros elementos del DOM o incluso se requerirá de JavaScript proveniente de otros módulos que deben estar cargados previamente.

Si este módulo del ejemplo que ahora está en línea, es decir, dentro del flujo del documento, estuviera en un script externo, podríamos vincularlo asíncronamente con <script src="modulo-lanzador.js" async></script>. Así se auto-cargaría la función lanzadora en cualquier momento antes del evento window.onload. Ya dentro de éste podríamos ejecutarlo con unLanzador = lanzador(...).

La ventaja de descargar todos los archivos .js de forma asíncrona es que pueden descargase en paralelo, con la consiguiente disminución en el tiempo de carga de la página. Pues en otro caso con cada descarga de un .js el navegador bloqueará el resto de recursos hasta que ese archivo .js haya sido descargado. Separando la ejecución que manipula el DOM de la propia descarga conseguimos que no haya interacciones que la dentengan. Cuando se dispara el evento window.onload todos los módulos habrán sido ya descargados y podemos lanzar o iniciar los que necesiten modificar el DOM.


Últimamente he estado trabajando en este sitio para que todos los archivos JS se descarguen de forma asíncrona, fijándome como objetivo que lo primero que se descargue sea el contenido HTML de la página y luego ejecutar los módulos JavaScript desde el window.onload. Este planteamiento y lo relativo al espacio de nombres que expondré en el siguiente tema son modificaciones que ya estoy probando en este sitio.