Módulos
Escreva código que seja fácil de deletar, não fácil de estender.

Idealmente, um programa tem uma estrutura clara e direta. A maneira como ele funciona é fácil de explicar, e cada parte desempenha um papel bem definido.
Na prática, programas crescem de forma orgânica. Partes de funcionalidade são adicionadas à medida que o programador identifica novas necessidades. Manter um programa assim bem estruturado exige atenção e trabalho constantes. Esse é um trabalho cujo retorno só aparece no futuro, na próxima vez que alguém trabalhar no programa, então é tentador negligenciá-lo e permitir que as várias partes do programa fiquem profundamente entrelaçadas.
Isso causa dois problemas práticos. Primeiro, entender um sistema entrelaçado é difícil. Se tudo pode tocar tudo, fica difícil analisar qualquer parte isoladamente. Você é forçado a construir uma compreensão holística do sistema inteiro. Segundo, se você quiser usar alguma funcionalidade desse programa em outra situação, reescrevê-la pode ser mais fácil do que tentar separá-la do seu contexto.
A expressão “grande bola de lama” é frequentemente usada para esse tipo de programa grande e sem estrutura. Tudo fica grudado, e quando você tenta extrair uma parte, o todo se desfaz, e você só consegue criar mais bagunça.
Programas modulares
Módulos são uma tentativa de evitar esses problemas. Um módulo é uma parte do programa que especifica de quais outras partes ele depende e qual funcionalidade ele oferece para que outros módulos utilizem (sua interface).
As interfaces de módulos têm muito em comum com interfaces de objetos, como vimos no Capítulo 6. Elas tornam parte do módulo disponível para o mundo externo e mantêm o restante privado.
Mas a interface que um módulo fornece para outros usarem é apenas metade da história. Um bom sistema de módulos também exige que os módulos especifiquem qual código eles utilizam de outros módulos. Essas relações são chamadas de dependências. Se o módulo A usa funcionalidade do módulo B, diz-se que ele depende desse módulo. Quando isso é claramente especificado no próprio módulo, pode ser usado para descobrir quais outros módulos precisam estar presentes para usar um determinado módulo e para carregar dependências automaticamente.
Quando as formas como os módulos interagem entre si são explícitas, um sistema se torna mais parecido com LEGO, onde as peças interagem por conectores bem definidos, e menos com lama, onde tudo se mistura com tudo.
Módulos ES
A linguagem JavaScript original não tinha nenhum conceito de módulo. Todos os scripts rodavam no mesmo escopo, e acessar uma função definida em outro script era feito referenciando as ligações globais criadas por esse script. Isso incentivava ativamente o entrelaçamento acidental e difícil de perceber do código e gerava problemas como scripts não relacionados tentando usar o mesmo nome de ligação.
Desde o ECMAScript 2015, o JavaScript suporta dois tipos diferentes de programas. Scripts se comportam da maneira antiga: suas ligações são definidas no escopo global, e eles não têm como referenciar diretamente outros scripts. Módulos têm seu próprio escopo separado e suportam as palavras-chave import e export, que não estão disponíveis em scripts, para declarar suas dependências e interface. Esse sistema de módulos é geralmente chamado de módulos ES (onde ES significa ECMAScript).
Um programa modular é composto por vários desses módulos, conectados por meio de seus imports e exports.
O exemplo de módulo a seguir converte entre nomes de dias e números (como retornados pelo método getDay de Date). Ele define uma constante que não faz parte de sua interface e duas funções que fazem. Não possui dependências.
const names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; export function dayName(number) { return names[number]; } export function dayNumber(name) { return names.indexOf(name); }
A palavra-chave export pode ser colocada antes da definição de uma função, classe ou ligação para indicar que aquela ligação faz parte da interface do módulo. Isso permite que outros módulos usem essa ligação ao importá-lo.
import {dayName} from "./dayname.js"; let now = new Date(); console.log(`Today is ${dayName(now.getDay())}`); // → Today is Monday
A palavra-chave import, seguida por uma lista de nomes de ligações entre chaves, torna ligações de outro módulo disponíveis no módulo atual. Os módulos são identificados por strings entre aspas.
A forma como esse nome de módulo é resolvido para um programa real varia conforme a plataforma. O navegador os trata como endereços web, enquanto o Node.js os resolve como arquivos. Quando você executa um módulo, todos os outros módulos dos quais ele depende — e os módulos dos quais estes dependem — são carregados, e as ligações exportadas são disponibilizadas aos módulos que os importam.
Declarações de import e export não podem aparecer dentro de funções, loops ou outros blocos. Elas são resolvidas imediatamente quando o módulo é carregado, independentemente de como o código dentro do módulo é executado. Por isso, devem aparecer apenas no corpo externo do módulo.
A interface de um módulo, portanto, consiste em uma coleção de ligações nomeadas que outros módulos que dependem dele podem acessar. Ligações importadas podem ser renomeados para receber um novo nome local usando as após seu nome.
import {dayName as nomDeJour} from "./dayname.js"; console.log(nomDeJour(3)); // → Wednesday
Um módulo também pode ter uma exportação especial chamada default, frequentemente usada para módulos que exportam apenas uma única ligação. Para definir uma exportação padrão, você escreve export default antes de uma expressão, declaração de função ou declaração de classe.
export default ["Winter", "Spring", "Summer", "Autumn"];
Essa ligação é importada omitindo as chaves ao redor do nome do import.
import seasonNames from "./seasonname.js";
Para importar todas as ligações de um módulo de uma só vez, você pode usar import *. Você fornece um nome, e esse nome será associado a um objeto contendo todas as exportações do módulo. Isso pode ser útil quando você usa muitas exportações diferentes.
import * as dayName from "./dayname.js"; console.log(dayName.dayName(3)); // → Wednesday
Pacotes
Uma das vantagens de construir um programa a partir de partes separadas e poder executar algumas dessas partes de forma independente é que você pode conseguir reutilizar a mesma parte em diferentes programas.
Mas como organizar isso? Digamos que eu queira usar a função parseINI do Capítulo 9 em outro programa. Se estiver claro de quais dependências a função precisa (neste caso, nenhuma), posso simplesmente copiar aquele módulo para meu novo projeto e usá-lo. Mas então, se eu encontrar um erro no código, provavelmente vou corrigi-lo no programa em que estou trabalhando naquele momento e esquecer de corrigi-lo no outro.
Assim que você começa a duplicar código, rapidamente se verá desperdiçando tempo e energia movendo cópias e mantendo-as atualizadas. É aí que entram os pacotes. Um pacote é um bloco de código que pode ser distribuído (copiado e instalado). Ele pode conter um ou mais módulos e possui informações sobre de quais outros pacotes depende. Um pacote geralmente também vem com documentação explicando o que ele faz, para que pessoas que não o escreveram ainda possam usá-lo.
Quando um problema é encontrado em um pacote ou uma nova funcionalidade é adicionada, o pacote é atualizado. Agora, os programas que dependem dele (que também podem ser pacotes) podem copiar a nova versão para obter as melhorias feitas no código.
Trabalhar dessa forma exige infraestrutura. Precisamos de um lugar para armazenar e encontrar pacotes e de uma maneira conveniente de instalá-los e atualizá-los. No mundo JavaScript, essa infraestrutura é fornecida pelo NPM (https://npmjs.com).
O NPM é duas coisas: um serviço online onde você pode baixar (e enviar) pacotes e um programa (incluído no Node.js) que ajuda a instalá-los e gerenciá-los.
No momento em que este texto foi escrito, há mais de três milhões de pacotes diferentes disponíveis no NPM. Uma grande parte deles é lixo, para ser justo. Mas quase todo pacote JavaScript útil disponível publicamente pode ser encontrado no NPM. Por exemplo, um parser de arquivos INI, semelhante ao que construímos no Capítulo 9, está disponível sob o nome de pacote ini.
O Capítulo 20 mostrará como instalar esses pacotes localmente usando o programa de linha de comando npm.
Ter pacotes de qualidade disponíveis para download é extremamente valioso. Isso significa que muitas vezes podemos evitar reinventar um programa que 100 pessoas já escreveram antes e obter uma implementação sólida e bem testada com apenas alguns comandos.
Software é barato de copiar, então, depois que alguém o escreve, distribuí-lo para outras pessoas é um processo eficiente. Escrevê-lo inicialmente dá trabalho, porém, e responder a pessoas que encontraram problemas no código ou que querem propor novas funcionalidades dá ainda mais trabalho.
Por padrão, você possui os direitos autorais do código que escreve, e outras pessoas só podem usá-lo com sua permissão. Mas como algumas pessoas são simplesmente legais e porque publicar bons softwares pode torná-lo um pouco conhecido entre programadores, muitos pacotes são publicados sob uma licença que permite explicitamente que outras pessoas os utilizem.
A maior parte do código no NPM é licenciada dessa forma. Algumas licenças exigem que você também publique o código que constrói com base no pacote sob a mesma licença. Outras são menos exigentes, requerendo apenas que você mantenha a licença junto com o código ao distribuí-lo. A comunidade JavaScript usa majoritariamente esse segundo tipo de licença. Ao usar pacotes de outras pessoas, certifique-se de conhecer suas licenças.
Agora, em vez de escrever nosso próprio parser de arquivos INI, podemos usar um do NPM.
import {parse} from "ini"; console.log(parse("x = 10\ny = 20")); // → {x: "10", y: "20"}
Módulos CommonJS
Antes de 2015, quando a linguagem JavaScript não tinha um sistema de módulos embutido, as pessoas já construíam sistemas grandes em JavaScript. Para tornar isso viável, elas precisavam de módulos.
A comunidade criou seus próprios sistemas de módulos improvisados sobre a linguagem. Eles usam funções para criar um escopo local para os módulos e objetos comuns para representar as interfaces dos módulos.
Inicialmente, as pessoas simplesmente envolviam manualmente todo o módulo em uma “expressão de função imediatamente invocada” para criar o escopo do módulo e atribuíram seus objetos de interface a uma única variável global.
const weekDay = function() { const names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; return { name(number) { return names[number]; }, number(name) { return names.indexOf(name); } }; }(); console.log(weekDay.name(weekDay.number("Sunday"))); // → Sunday
Esse estilo de módulos fornece isolamento até certo ponto, mas não declara dependências. Em vez disso, apenas coloca sua interface no escopo global e espera que suas dependências, se houver, façam o mesmo. Isso não é ideal.
Se implementarmos nosso próprio carregador de módulos, podemos fazer melhor. A abordagem mais amplamente utilizada para módulos JavaScript acoplados posteriormente é chamada de módulos CommonJS. O Node.js usou esse sistema de módulos desde o início (embora agora também saiba carregar módulos ES), e ele é o sistema usado por muitos pacotes no NPM.
Um módulo CommonJS parece um script comum, mas tem acesso a duas ligações que usa para interagir com outros módulos. A primeira é uma função chamada require. Quando você a chama com o nome do módulo da sua dependência, ela garante que o módulo seja carregado e retorna sua interface. A segunda é um objeto chamado exports, que é o objeto de interface do módulo. Ele começa vazio, e você adiciona propriedades a ele para definir valores exportados.
Este exemplo de módulo CommonJS fornece uma função de formatação de data. Ele usa dois pacotes do NPM — ordinal para converter números em strings como "1st" e "2nd", e date-names para obter os nomes em inglês de dias da semana e meses. Ele exporta uma única função, formatDate, que recebe um objeto Date e uma string de template.
A string de template pode conter códigos que orientam a formatação, como YYYY para o ano completo e Do para o dia ordinal do mês. Você pode fornecer uma string como "MMMM Do YYYY" para obter uma saída como November 22nd 2017.
const ordinal = require("ordinal"); const {days, months} = require("date-names"); exports.formatDate = function(date, format) { return format.replace(/YYYY|M(MMM)?|Do?|dddd/g, tag => { if (tag == "YYYY") return date.getFullYear(); if (tag == "M") return date.getMonth(); if (tag == "MMMM") return months[date.getMonth()]; if (tag == "D") return date.getDate(); if (tag == "Do") return ordinal(date.getDate()); if (tag == "dddd") return days[date.getDay()]; }); };
A interface de ordinal é uma única função, enquanto date-names exporta um objeto contendo várias coisas — days e months são arrays de nomes. Desestruturação é muito conveniente ao criar ligações para interfaces importadas.
O módulo adiciona sua função de interface a exports para que módulos que dependem dele tenham acesso a ela. Podemos usar o módulo assim:
const {formatDate} = require("./format-date.js"); console.log(formatDate(new Date(2017, 9, 13), "dddd the Do")); // → Friday the 13th
CommonJS é implementado com um carregador de módulos que, ao carregar um módulo, envolve seu código em uma função (dando a ele seu próprio escopo local) e passa as ligações require e exports para essa função como argumentos.
Se assumirmos que temos acesso a uma função readFile que lê um arquivo pelo nome e nos fornece seu conteúdo, podemos definir uma forma simplificada de require assim:
function require(name) { if (!(name in require.cache)) { let code = readFile(name); let exports = require.cache[name] = {}; let wrapper = Function("require, exports", code); wrapper(require, exports); } return require.cache[name]; } require.cache = Object.create(null);
Function é uma função embutida do JavaScript que recebe uma lista de argumentos (como uma string separada por vírgulas) e uma string contendo o corpo da função e retorna um valor de função com esses argumentos e esse corpo. Esse é um conceito interessante — ele permite que um programa crie novas partes de programa a partir de dados em forma de string — mas também é perigoso, pois se alguém conseguir enganar seu programa para colocar uma string fornecida por essa pessoa em Function, ela poderá fazer o programa executar qualquer coisa que quiser.
O JavaScript padrão não fornece uma função como readFile, mas diferentes ambientes JavaScript, como o navegador e o Node.js, fornecem suas próprias maneiras de acessar arquivos. O exemplo apenas finge que readFile existe.
Para evitar carregar o mesmo módulo várias vezes, require mantém um armazenamento (cache) de módulos já carregados. Quando chamado, ele primeiro verifica se o módulo solicitado já foi carregado e, se não, o carrega. Isso envolve ler o código do módulo, envolvê-lo em uma função e executá-lo.
Ao definir require e exports como parâmetros da função wrapper gerada (e passando os valores apropriados ao chamá-la), o carregador garante que essas ligações estejam disponíveis no escopo do módulo.
Uma diferença importante entre esse sistema e os módulos ES é que as importações de módulos ES acontecem antes que o script do módulo comece a executar, enquanto require é uma função normal, chamada quando o módulo já está em execução. Diferentemente das declarações import, chamadas a require podem aparecer dentro de funções, e o nome da dependência pode ser qualquer expressão que resulte em uma string, enquanto import permite apenas strings literais.
A transição da comunidade JavaScript do estilo CommonJS para módulos ES foi lenta e um pouco turbulenta. Felizmente, agora estamos em um ponto em que a maioria dos pacotes populares no NPM fornece seu código como módulos ES, e o Node.js permite que módulos ES importem de módulos CommonJS. Embora você ainda encontre código CommonJS, não há motivo real para escrever novos programas nesse estilo.
Construção e empacotamento
Muitos pacotes JavaScript não são tecnicamente escritos em JavaScript. Extensões de linguagem como TypeScript, o dialeto com verificação de tipos mencionado no Capítulo 8, são amplamente utilizadas. As pessoas também costumam usar novos recursos da linguagem planejados muito antes de eles serem adicionados às plataformas que realmente executam JavaScript. Para tornar isso possível, elas compilam seu código, traduzindo-o de seu dialeto de JavaScript escolhido para JavaScript puro — ou até para uma versão mais antiga do JavaScript — para que os navegadores possam executá-lo.
Incluir um programa modular composto por 200 arquivos diferentes em uma página web traz seus próprios problemas. Se buscar um único arquivo pela rede leva 50 milissegundos, carregar o programa inteiro leva 10 segundos, ou talvez metade disso se você puder carregar vários arquivos simultaneamente. Isso é muito tempo desperdiçado. Como buscar um único arquivo grande tende a ser mais rápido do que buscar muitos pequenos, programadores web começaram a usar ferramentas que combinam seus programas (que foram cuidadosamente divididos em módulos) em um único arquivo grande antes de publicá-los na web. Essas ferramentas são chamadas de bundlers (empacotadores).
E podemos ir além. Além do número de arquivos, o tamanho dos arquivos também determina a velocidade com que podem ser transferidos pela rede. Assim, a comunidade JavaScript inventou os minifiers (minificadores). São ferramentas que pegam um programa JavaScript e o tornam menor removendo automaticamente comentários e espaços em branco, renomeando ligações e substituindo trechos de código por equivalentes que ocupam menos espaço.
Não é incomum que o código que você encontra em um pacote NPM ou que roda em uma página web tenha passado por múltiplas etapas de transformação — convertendo de JavaScript moderno para JavaScript antigo, combinando módulos em um único arquivo e minimizando o código. Não entraremos nos detalhes dessas ferramentas neste livro, já que existem muitas, e a popularidade delas muda com frequência. Apenas saiba que essas coisas existem e procure por elas quando precisar.
Design de módulos
Estruturar programas é um dos aspectos mais sutis da programação. Qualquer funcionalidade não trivial pode ser organizada de várias maneiras.
Um bom design de programa é subjetivo — há compromissos envolvidos e questões de gosto. A melhor maneira de aprender o valor de uma boa estrutura é ler ou trabalhar em muitos programas e observar o que funciona e o que não funciona. Não presuma que uma bagunça dolorosa é “simplesmente assim mesmo”. Você pode melhorar a estrutura de quase tudo colocando mais reflexão nisso.
Um aspecto do design de módulos é a facilidade de uso. Se você está projetando algo que será usado por várias pessoas — ou até por você mesmo, daqui a três meses, quando já não lembrar dos detalhes do que fez — é útil que sua interface seja simples e previsível.
Isso pode significar seguir convenções existentes. Um bom exemplo é o pacote ini. Esse módulo imita o objeto padrão JSON, fornecendo funções parse e stringify (para escrever um arquivo INI) e, assim como JSON, converte entre strings e objetos simples. A interface é pequena e familiar, e depois que você a usa uma vez, provavelmente vai lembrar como usá-la.
Mesmo que não haja uma função padrão ou pacote amplamente utilizado para imitar, você pode manter seus módulos previsíveis usando estrutura de dados simples e fazendo uma única tarefa bem definida. Muitos dos módulos de leitura de arquivos INI no NPM fornecem uma função que lê diretamente esse tipo de arquivo do disco, por exemplo. Isso torna impossível usar esses módulos no navegador, onde não temos acesso direto ao sistema de arquivos, e adiciona complexidade que seria melhor resolvida compondo o módulo com alguma função de leitura de arquivos.
Isso aponta para outro aspecto útil do design de módulos — a facilidade com que algo pode ser composto com outro código. Módulos focados que computam valores são aplicáveis em uma gama maior de programas do que módulos maiores que executam ações complicadas com efeitos colaterais. Um leitor de arquivos INI que insiste em ler o arquivo do disco é inútil em um cenário onde o conteúdo do arquivo vem de outra fonte.
De forma relacionada, objetos com estado às vezes são úteis ou até necessários, mas se algo pode ser feito com uma função, use uma função. Vários leitores de arquivos INI no NPM fornecem um estilo de interface que exige que você primeiro crie um objeto, depois carregue o arquivo nele e, por fim, use métodos específicos para obter os resultados. Esse tipo de coisa é comum na tradição orientada a objetos, e é terrível. Em vez de fazer uma única chamada de função e seguir em frente, você precisa realizar o ritual de mover seu objeto por diferentes estados. E, como os dados agora estão encapsulados em um tipo de objeto especializado, todo código que interage com eles precisa conhecer esse tipo, criando interdependências desnecessárias.
Frequentemente, definir novas estruturas de dados é inevitável — apenas algumas básicas são fornecidas pelo padrão da linguagem, e muitos tipos de dados precisam ser mais complexos do que um array ou um map. Mas quando um array é suficiente, use um array.
Um exemplo de estrutura de dados um pouco mais complexa é o grafo do Capítulo 7. Não há uma única maneira óbvia de representar um grafo em JavaScript. Naquele capítulo, usamos um objeto cujas propriedades contêm arrays de strings — os outros nós alcançáveis a partir daquele nó.
Existem vários pacotes de busca de caminho no NPM, mas nenhum deles usa esse formato de grafo. Eles geralmente permitem que as arestas do grafo tenham um peso, que é o custo ou distância associado a elas. Isso não é possível na nossa representação.
Por exemplo, existe o pacote dijkstrajs. Uma abordagem bem conhecida para busca de caminhos, bastante semelhante à nossa função findRoute, é chamada de algoritmo de Dijkstra, em homenagem a Edsger Dijkstra, que o descreveu pela primeira vez. O sufixo js é frequentemente adicionado a nomes de pacotes para indicar que são escritos em JavaScript. Esse pacote dijkstrajs usa um formato de grafo semelhante ao nosso, mas, em vez de arrays, usa objetos cujos valores das propriedades são números — os pesos das arestas.
Se quiséssemos usar esse pacote, teríamos que garantir que nosso grafo estivesse no formato que ele espera. Todas as arestas recebem o mesmo peso, já que nosso modelo simplificado trata cada estrada como tendo o mesmo custo (um turno).
const {find_path} = require("dijkstrajs"); let graph = {}; for (let node of Object.keys(roadGraph)) { let edges = graph[node] = {}; for (let dest of roadGraph[node]) { edges[dest] = 1; } } console.log(find_path(graph, "Post Office", "Cabin")); // → ["Post Office", "Alice's House", "Cabin"]
Isso pode ser uma barreira para composição — quando diferentes pacotes usam estruturas de dados diferentes para descrever coisas semelhantes, combiná-los se torna difícil. Portanto, se você quiser projetar pensando em composição, descubra quais estrutura de dados outras pessoas estão usando e, quando possível, siga esse exemplo.
Projetar uma estrutura de módulos adequada para um programa pode ser difícil. Na fase em que você ainda está explorando o problema, tentando coisas diferentes para ver o que funciona, pode ser melhor não se preocupar muito com isso, já que manter tudo organizado pode ser uma grande distração. Quando você tiver algo que pareça sólido, esse é um bom momento para dar um passo atrás e organizar.
Resumo
Módulos fornecem estrutura a programas maiores ao separar o código em partes com interfaces e dependências claras. A interface é a parte do módulo visível para outros módulos, e as dependências são os outros módulos que ele utiliza.
Como o JavaScript historicamente não fornecia um sistema de módulos, o sistema CommonJS foi construído sobre ele. Depois, em certo momento, ele passou a ter um sistema embutido, que agora coexiste de forma um tanto desconfortável com o CommonJS.
Um pacote é um bloco de código que pode ser distribuído de forma independente. NPM é um repositório de pacotes JavaScript. Você pode baixar todo tipo de pacote útil (e inútil) a partir dele.
Exercícios
Um robô modular
Estas são as ligações que o projeto do Capítulo 7 cria:
roads buildGraph roadGraph VillageState runRobot randomPick randomRobot mailRoute routeRobot findRoute goalOrientedRobot
Se você fosse escrever esse projeto como um programa modular, quais módulos criaria? Qual módulo dependeria de qual outro módulo, e como seriam suas interfaces?
Quais partes provavelmente já estariam disponíveis prontas no NPM? Você preferiria usar um pacote do NPM ou escrevê-las por conta própria?
Mostrar dicas...
Aqui está o que eu faria (mas, novamente, não existe uma única forma correta de projetar um módulo):
O código usado para construir o grafo das estradas ficaria no módulo graph.js. Como eu prefiro usar dijkstrajs do NPM em vez do nosso próprio código de busca de caminho, faremos com que ele construa o tipo de estrutura de grafo que o dijkstrajs espera. Esse módulo exporta uma única função, buildGraph. Eu faria buildGraph aceitar um array de arrays de dois elementos, em vez de strings contendo hífens, para tornar o módulo menos dependente do formato de entrada.
O módulo roads.js contém os dados brutos das estradas (o array roads) e a ligação roadGraph. Esse módulo depende de ./graph.js e exporta o grafo das estradas.
A classe VillageState fica no módulo state.js. Ela depende do módulo ./roads.js porque precisa verificar se uma determinada estrada existe. Também precisa de randomPick. Como essa é uma função de três linhas, poderíamos simplesmente colocá-la dentro do módulo state.js como uma função auxiliar interna. Mas randomRobot também precisa dela. Então teríamos que duplicá-la ou colocá-la em seu próprio módulo. Como essa função existe no NPM no pacote random-item, uma solução razoável é fazer com que ambos os módulos dependam dele. Também podemos adicionar a função runRobot a esse módulo, já que ela é pequena e está intimamente relacionada ao gerenciamento de estado. O módulo exporta tanto a classe VillageState quanto a função runRobot.
Por fim, os robôs, juntamente com os valores de que dependem, como mailRoute, poderiam ir para um módulo example-robots., que depende de ./roads.js e exporta as funções dos robôs. Para permitir que goalOrientedRobot faça busca de rota, esse módulo também depende de dijkstrajs.
Ao delegar parte do trabalho para módulos do NPM, o código ficou um pouco menor. Cada módulo individual faz algo relativamente simples e pode ser lido isoladamente. Dividir o código em módulos também frequentemente sugere melhorias adicionais no design do programa. Nesse caso, parece um pouco estranho que VillageState e os robôs dependam de um grafo de estradas específico. Pode ser melhor tornar o grafo um argumento para o construtor do estado e fazer com que os robôs o leiam do objeto de estado — isso reduz dependências (o que é sempre bom) e possibilita executar simulações em diferentes mapas (o que é ainda melhor).
É uma boa ideia usar módulos do NPM para coisas que poderíamos escrever nós mesmos? Em princípio, sim — para coisas não triviais como a função de busca de caminho, é provável que você cometa erros e desperdice tempo escrevendo por conta própria. Para funções pequenas como random-item, escrevê-las você mesmo é fácil. Mas adicioná-las em todos os lugares onde são necessárias tende a poluir seus módulos.
No entanto, você também não deve subestimar o trabalho envolvido em encontrar um pacote adequado no NPM. E mesmo que encontre um, ele pode não funcionar bem ou pode faltar alguma funcionalidade de que você precisa. Além disso, depender de pacotes do NPM significa que você precisa garantir que eles estejam instalados, distribuí-los com seu programa e possivelmente atualizá-los periodicamente.
Então, novamente, isso é um equilíbrio, e você pode decidir de uma forma ou de outra dependendo de quanto um determinado pacote realmente ajuda.
Módulo de estradas
Escreva um módulo ES baseado no exemplo do Capítulo 7 que contenha o array de estradas e exporte a estrutura de dados de grafo que as representa como roadGraph. Ele depende de um módulo ./graph.js que exporta uma função buildGraph, usada para construir o grafo. Essa função espera um array de arrays de dois elementos (os pontos inicial e final das estradas).
// Adicione dependências e exports const roads = [ "Alice's House-Bob's House", "Alice's House-Cabin", "Alice's House-Post Office", "Bob's House-Town Hall", "Daria's House-Ernie's House", "Daria's House-Town Hall", "Ernie's House-Grete's House", "Grete's House-Farm", "Grete's House-Shop", "Marketplace-Farm", "Marketplace-Post Office", "Marketplace-Shop", "Marketplace-Town Hall", "Shop-Town Hall" ];
Mostrar dicas...
Como este é um módulo ES, você deve usar import para acessar o módulo de grafo. Ele foi descrito como exportando uma função buildGraph, que você pode extrair de seu objeto de interface usando uma declaração const com desestruturação.
Para exportar roadGraph, você coloca a palavra-chave export antes de sua definição. Como buildGraph recebe uma estrutura de dados que não corresponde exatamente a roads, a divisão das strings de estradas deve acontecer no seu módulo.
Dependências circulares
Uma dependência circular é uma situação em que o módulo A depende de B, e B também, direta ou indiretamente, depende de A. Muitos sistemas de módulos simplesmente proíbem isso porque, independentemente da ordem escolhida para carregar esses módulos, não é possível garantir que as dependências de cada módulo tenham sido carregadas antes de sua execução.
CommonJS modules permitem uma forma limitada de dependências cíclicas. Desde que os módulos não acessem a interface um do outro até que terminem de carregar, dependências cíclicas são aceitáveis.
A função require apresentada anteriormente neste capítulo suporta esse tipo de ciclo de dependência. Você consegue ver como ela lida com ciclos?
Mostrar dicas...
O truque é que require adiciona o objeto de interface de um módulo ao seu cache antes de começar a carregá-lo. Dessa forma, se alguma chamada a require feita durante sua execução tentar carregá-lo, ele já é conhecido, e a interface atual será retornada, em vez de iniciar o carregamento do módulo novamente (o que acabaria estourando a pilha).