Modularização implica na divisão das funcionalidades de um código em partes distintas. Os módulos compõe peças que podem ser adicionadas e removidas quando necessário, vejam: reuso de código.
Os frutos do encapsulamento alcançado com a modularização são a redução da complexidade, separação de interesses e manutenção descomplicada. Ainda, a definição de cada módulo força o programador a determinar quais os limites e responsabilidades de cada porção do código.
Acredito que estes argumentos já justificam a adoção de um sistema de módulos para seu código. Assumindo que estamos escrevendo código segundo a especificação ECMAScript 5, tudo começa por uma das palavras grifadas no início do texto: encapsulamento.
Encapsulamento em JavaScript
Todo programador que se depara com um código, por menos complexo que seja, precisará entender o conceito de escopo. O escopo de uma variável ou função no JavaScript são as linhas de código em que estas são acessíveis.
Encapsulamento é um dos fundamentos da programação orientada a objetos tradicional. Considerando que não temos classes no JavaScript e se entendermos encapsulamento como uma forma de restringir acesso à informação, concluímos que a definição de escopo é o caminho para alcança-lo.
O JavaScript possui um escopo global, que quando em navegadores é window
, e aqueles criados a partir da execução de uma função. A maneira mais fácil de alcançar encapsulamento é utilizando uma função anônima invocada imediatamente após sua definição:
Por favor, saiba que este pattern chama-se Immediately-Invoked Function Expression (IIFE) e que os parênteses iniciais servem para que a instrução seja reconhecida como uma expressão.
Módulos
Mencionado já há mais de 10 anos, o mais simples dos padrões de escrita de módulos em JavaScript é o module pattern. O padrão consiste de uma IIFE que retorna um objeto com valores e funções, veja:
Revealing Module Pattern
O module pattern possui muitas variações, uma delas é o revealing module pattern. Neste padrão, todas as funções e valores do módulo são acessíveis no escopo local e apenas referências são retornadas na forma de objeto.
Apesar de mencionado no artigo que define o padrão, o ideal é retornar apenas referências a funções, retornar outros valores pode dar dor de cabeça. Revealing module pattern é bastante interessante pela garantia de acesso descomplicado sem a necessidade de uso do this
, por exemplo. A propósito, este conceito será útil para a construção de módulos melhores em outros padrões.
Namespace
Os padrões que vimos até então poluem o escopo global da aplicação com a definição de uma série de variáveis. Uma solução é a criação de um namespace de uso específico para os módulos.
Módulos robustos
É natural que módulos dependam uns dos outros. Uma característica importante de um sistema de módulos robusto é a possibilidade de indicar quais são as dependências de um determinado módulo e traçar uma estratégia de carregamento caso esta não esteja disponível.
Asyncronous Module Definition (AMD)
Módulos AMD podem ser requisitados, definidos e utilizados a medida que necessários. Nosso contador, se reescrito em AMD, ficaria da seguinte maneira:
Diferente de outros sistemas de módulos, as dependências de cada módulo AMD são indicadas na sua própria definição. Isto significa que as dependências não precisam estar prontas para o uso assim que o módulo seja definido, estas podem ser carregadas assincronamente condicionando a execução do módulo.
O trecho de código a seguir define um módulo com duas dependências:
Caso tenha achado estranho, saiba que a definição do módulo geralmente utiliza uma formatação de espaços bastante específica para facilitar a leitura e entendimento das dependências.
Em meio ao trecho de código, caso não tenha notado, não definimos o identificador deste módulo. Os módulos podem (e devem) ser definidos um em cada arquivo e, nestes casos, o nome do arquivo torna-se o identificador.
Requisitando módulos
Toda aplicação terá um trecho principal de código que irá requisitar os módulos e fazer o bootstrap da aplicação. O require
(sim, a semântica é lógica) não exige identificação e atende ao requisito, veja a seguir:
Uma boa prática é criar uma interface comum para iniciar o comportamento de cada um dos módulos. Existem diferentes preferências, particularmente utilizo uma função init
. Desta forma, o corpo de código do require
conteria algo como slider.init()
e counter.init()
.
Carregando os módulos
Módulos AMD podem ser utilizados em qualquer navegador, porém sua definição não é suportada nativamente. O que significa que precisamos de uma biblioteca que provenha as tais funções define
e require
. O mais popular loader
de AMD é o require.js. Desculpe decepcionar, mas sua configuração não está no escopo deste texto.
Empacotando os módulos
Um dos principais argumentos contra o uso de AMD é a demora para o carregamento de todos os módulos e suas dependências. Apesar de possível, o carregamento assíncrono de cada um dos módulos é sumariamente não recomendado levando em conta os protocolos de rede que utilizamos atualmente.
Existem ferramentas como o r.js que tem a função de empacotar os módulos e entregar um único arquivo para donwload no client-side. O r.js depende de Node.js e introduz uma etapa de análise e concatenação dos arquivos.
Caso já possua um workflow para cuidar do seus assets, concatenar os arquivos e utilizar o almond pode ser uma solução bem mais simples. O único detalhe é que você precisará atribuir um identificador para cada módulo. Mesmo que os módulos estejam definidos cada um em um arquivo, lembre-se que a biblioteca entrará em ação apenas após a concatenação.
Motivação para o uso
Os módulos AMD já são utilizados nos mais famosos projetos escritos em JavaScript, basta acessar os repositórios: jQuery, Flight, Lo-Dash, Mout; e muitos outros.
A definição permite ainda definir plugins para estender o comportamento de carregamento dos módulos e carregar outros conteúdos que não sejam unicamente JavaScript.
O futuro
A especificação ECMAScript 6 já possui um rascunho de uma nova definição de módulos. Os assim chamados ES6 modules são baseados em um sistema de módulos robusto da especificação CommonJS.
CommonJS modules
Os módulos CJS se tornaram bastante populares por serem a base do padrão adotado pelo Node.js. O principal impedimento para seu uso atualmente é o não suporte a este padrão nos navegadores. Existem ferramentas para viabilizar o seu uso em navegadores, inclusive módulos AMD podem internamente utilizar a sintaxe proposta pela especificação de CommonJS.
ES6 modules
O sistema de módulos ES6 combina os dois melhores sistemas de módulos existentes. A definição dos módulos se parece muito com CJS e as mesmas características assíncronas da AMD são endereçadas pela especificação de um loader nativo.
[ES6 modules] permitirão a definição de dependências sem uso de callbacks (como em CJS) mas serão assíncronos (como AMD). – David Herman
Considerações finais
Um sistema de módulos adequado é a saída para manter a sanidade do seu código JavaScript. A despeito das inúmeras soluções, saiba que a escolha de um sistema já padronizado garante o uso de uma solução otimizada para este problema comum.
Se me permite um conselho: prefira sempre um sistema de módulos robusto, é difícil prever o quão complexa uma aplicação poderá se tornar. Em outras palavras, escolha entre: AMD, CommonJS modules utilizando ferramentas como Browserify e até mesmo (por sua conta e risco) ES6 modules com ES6 Module Loader.