第4版が利用可能です。 こちらで読むことができます!

第10章モジュール

拡張しやすいコードではなく、削除しやすいコードを書こう。

Tef, プログラミングはひどい
Picture of a building built from modular pieces

理想的なプログラムは、非常に明確な構造を持っている。その仕組みは説明しやすく、それぞれの部分は明確に定義された役割を果たす。

典型的な実際のプログラムは、有機的に成長する。新しいニーズが出てくるたびに、新しい機能が追加される。構造化、そして構造の維持は追加作業である。それは、将来、次回誰かがそのプログラムに取り組むときにのみ報われる作業である。そのため、それを怠り、プログラムの各部分が深く絡み合うままにしておくのは魅力的である。

これは、2つの実際的な問題を引き起こす。第一に、そのようなシステムを理解することは難しい。すべてが他のすべてに影響を与える可能性がある場合、特定の部分を単独で見ることは困難である。全体を包括的に理解する必要がある。第二に、そのようなプログラムの機能の一部を別の状況で使用したい場合、コンテキストから切り離そうとするよりも、書き直す方が簡単な場合がある。

そのような大きく、構造化されていないプログラムは、しばしば「大きな泥の塊」と呼ばれる。すべてがくっついており、一部分を取り出そうとすると、全体が崩れて手が汚れてしまう。

モジュール

モジュールは、これらの問題を回避しようとする試みである。モジュールとは、どの他の部分に依存し、他のモジュールが使用できるどの機能(そのインターフェース)を提供するかを指定するプログラムの一部である。

モジュールインターフェースは、第6章で見たように、オブジェクトインターフェースと多くの共通点を持っている。モジュールの一部を外部に公開し、残りの部分を非公開にする。モジュール間の相互作用の方法を制限することで、システムは、すべてがすべてと混ざり合う泥のようではなく、明確に定義されたコネクタを介して部品が相互作用するレゴのようになる。

モジュール間の関係は、依存関係と呼ばれる。モジュールが別のモジュールの一部を必要とする場合、そのモジュールに依存していると言われる。この事実がモジュール自体に明確に指定されている場合、特定のモジュールを使用するためにどの他のモジュールが存在する必要があるかを把握し、依存関係を自動的にロードするために使用できる。

モジュールをこのように分離するには、それぞれに独自のプライベートスコープが必要である。

JavaScriptコードを異なるファイルに配置するだけでは、これらの要件を満たせない。ファイルは依然として同じグローバル名前空間を共有している。意図的または偶発的に、互いのバインディングに干渉する可能性がある。そして、依存関係の構造は不明瞭なままである。この章で後述するように、私たちはもっとうまくできる。

プログラムに適したモジュール構造を設計することは難しい場合がある. 問題をまだ模索している段階、何がうまくいくか試している段階では、大きな邪魔になる可能性があるので、あまり気にしない方が良いかもしれない. しっかりとしたものができたら、一歩下がって整理する良いタイミングだ.

パッケージ

プログラムを別々の部分で構築し、実際にそれらの部分を単独で実行できることの利点の1つは、異なるプログラムで同じ部分を利用できる可能性があることだ。

しかし、どのように設定するのか? 第9章の`parseINI`関数を別のプログラムで使用したいとしよう。関数が何に依存しているか(この場合は何も依存していない)が明確であれば、必要なコードをすべて新しいプロジェクトにコピーして使用すればよい。しかし、そのコードに間違いが見つかった場合、おそらくその時点で作業しているプログラムで修正し、他のプログラムでも修正することを忘れてしまうだろう。

コードの複製を始めると、すぐにコピーの移動と最新の状態の維持に時間とエネルギーを浪費していることに気づくでしょう。

そこでパッケージの出番となる。パッケージとは、配布(コピーとインストール)できるコードの塊である。1つ以上のモジュールを含んでおり、どの他のパッケージに依存しているかについての情報を持っている。パッケージには通常、何をするのかを説明するドキュメントが付属しているので、作成者以外の人でも使用できるようになっている。

パッケージに問題が見つかった場合、または新しい機能が追加された場合、パッケージは更新される。これで、それに依存するプログラム(これもパッケージである可能性がある)は、新しいバージョンにアップグレードできる.

このように作業するには、インフラストラクチャが必要である。パッケージを保管して見つける場所と、パッケージをインストールしてアップグレードするための便利な方法が必要である。JavaScriptの世界では、このインフラストラクチャはNPM(https://npmjs.org)によって提供されている。

NPMは2つのものです。パッケージをダウンロード(およびアップロード)できるオンラインサービスと、パッケージのインストールと管理を支援するプログラム(Node.jsにバンドルされている)です。

執筆時点で、NPMでは50万種類以上のパッケージが利用可能である。その多くはゴミであることは言及しておかなければならないが、ほとんどすべての有用で公開されているパッケージはそこで見つけることができる。第9章で作成したものと同様のINIファイルパーサーは、パッケージ名`ini`で利用できる。

第20章では、`npm`コマンドラインプログラムを使用して、そのようなパッケージをローカルにインストールする方法を示す。

ダウンロード可能な高品質のパッケージが利用できることは非常に貴重である。それは、100人が以前に書いたプログラムを再発明することを避け、数回のキー操作で堅牢で十分にテストされた実装を得ることができることを意味する.

ソフトウェアのコピーにかかる費用は安いため、誰かがソフトウェアを作成したら、他の人に配布するのは効率的なプロセスです. しかし、最初にそれを書くこと仕事であり、コードに問題を見つけた人、または新しい機能を提案したい人に対応することは、さらに多くの仕事です.

デフォルトでは、自分が書いたコードの著作権は自分が所有しており、他の人はあなたの許可を得てのみ使用できます. しかし、親切な人がいることと、優れたソフトウェアを公開することでプログラマーの間で少し有名になれることから、多くのパッケージは他の人が明示的に使用できるライセンスの下で公開されています.

NPMのほとんどのコードはこのようにライセンスされています. 一部のライセンスでは、パッケージの上に構築したコードも同じライセンスで公開する必要があります. その他はそれほど厳しくなく、配布時にコードにライセンスを付けておくことだけが必要です. JavaScriptコミュニティは、ほとんどの場合後者のタイプのライセンスを使用しています. 他人のパッケージを使用する場合は、ライセンスに注意してください.

即席モジュール

2015年まで、JavaScript言語には組み込みのモジュールシステムがありませんでした。それでも、人々は10年以上JavaScriptで大規模なシステムを構築してきました。そして、彼らはモジュールを必要としていました

そこで、彼らは言語の上に独自のモジュールシステムを設計しました。JavaScript関数を使用してローカルスコープを作成し、オブジェクトを使用してモジュールインターフェースを表すことができます。

これは、曜日名と数値(`Date`の`getDay`メソッドによって返される)の間を行き来するためのモジュールです。そのインターフェースは`weekDay.name`と`weekDay.number`で構成され、ローカルバインディング`names`をすぐに呼び出される関数式のスコープ内に隠します。

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

このスタイルのモジュールはある程度の分離を提供しますが、依存関係を宣言しません。代わりに、インターフェースをグローバルスコープに配置し、依存関係がある場合は、依存関係も同じようにすることを期待します。長い間、これはWebプログラミングで使用される主なアプローチでしたが、現在ではほとんど obsolete です。

依存関係をコードの一部にしたい場合は、依存関係の読み込みを制御する必要があります。そのためには、文字列をコードとして実行できる必要があります。JavaScriptはこれを行うことができます。

データをコードとして評価する

データ(コードの文字列)を取得して現在のプログラムの一部として実行するには、いくつかの方法があります。

最も明白な方法は、特別な演算子`eval`です。これは、*現在の*スコープで文字列を実行します。これは通常、スコープが通常持っているいくつかのプロパティ(たとえば、特定の名前がどのバインディングを参照しているかを簡単に予測できるなど)を壊してしまうため、悪い考えです。

const x = 1;
function evalAndReturnX(code) {
  eval(code);
  return x;
}

console.log(evalAndReturnX("var x = 2"));
// → 2
console.log(x);
// → 1

データをコードとして解釈するそれほど恐ろしくない方法は、`Function`コンストラクターを使用することです。これは2つの引数を取ります。コンマ区切りの引数名リストを含む文字列と、関数本体を含む文字列です。コードを関数値でラップして、独自のスコープを取得し、他のスコープで奇妙なことをしないようにします。

let plusOne = Function("n", "return n + 1;");
console.log(plusOne(4));
// → 5

これはまさにモジュールシステムに必要なものです。モジュールのコードを関数でラップし、その関数のスコープをモジュールスコープとして使用できます.

CommonJS

ボルトオン式JavaScriptモジュールで最も広く使用されているアプローチは、*CommonJSモジュール*と呼ばれています。Node.jsはこれを使用しており、NPMのほとんどのパッケージで使用されているシステムです。

CommonJSモジュールの主要な概念は、`require`と呼ばれる関数です。これを依存関係のモジュール名で呼び出すと、モジュールがロードされ、そのインターフェースが返されます。

ローダーはモジュールコードを関数でラップするため、モジュールは自動的に独自のローカルスコープを取得します。依存関係にアクセスするには`require`を呼び出し、インターフェースを`exports`にバインドされたオブジェクトに配置するだけです。

このサンプルモジュールは、日付フォーマット関数を提供します。NPM から2つのパッケージを使用しています。—数字を"1st""2nd"のような文字列に変換するordinalと、曜日と月の英語名を取得するdate-namesです。このモジュールは、Dateオブジェクトとテンプレート文字列を引数に取る単一の関数formatDateをエクスポートします。

テンプレート文字列には、フォーマットを指示するコードを含めることができます。例えば、YYYYは西暦4桁を表し、Doは月の序数を表します。“November 22nd 2017”のような出力を得るには、"MMMM Do YYYY"のような文字列を指定します。

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()];
  });
};

ordinalのインターフェースは単一の関数ですが、date-namesは複数の要素を含むオブジェクトをエクスポートします。—daysmonthsは名前の配列です。インポートされたインターフェースのバインディングを作成する際に、分割代入は非常に便利です。

このモジュールは、インターフェース関数をexportsに追加することで、このモジュールに依存するモジュールがアクセスできるようにします。このモジュールは、次のように使用できます。

const {formatDate} = require("./format-date");

console.log(formatDate(new Date(2017, 9, 13),
                       "dddd the Do"));
// → Friday the 13th

最も単純な形式のrequireは、次のように定義できます。

require.cache = Object.create(null);

function require(name) {
  if (!(name in require.cache)) {
    let code = readFile(name);
    let module = {exports: {}};
    require.cache[name] = module;
    let wrapper = Function("require, exports, module", code);
    wrapper(require, module.exports, module);
  }
  return require.cache[name].exports;
}

このコードでは、readFileはファイルを読み込み、その内容を文字列として返す架空の関数です。標準のJavaScriptはこのような機能を提供していません。—しかし、ブラウザやNode.jsなど、異なるJavaScript環境は、独自のファイルアクセス方法を提供しています。この例では、readFileが存在すると仮定しています。

同じモジュールを複数回ロードしないように、requireは既にロードされたモジュールのストア(キャッシュ)を保持します。呼び出されると、最初に要求されたモジュールがロードされているかどうかを確認し、ロードされていない場合はロードします。これには、モジュールのコードを読み込み、関数でラップして、呼び出すことが含まれます。

前述のordinalパッケージのインターフェースは、オブジェクトではなく関数です。CommonJSモジュールの特異な点は、モジュールシステムが空のインターフェースオブジェクト(exportsにバインドされている)を作成しますが、module.exportsを上書きすることで、その値を任意の値に置き換えることができることです。これは、多くのモジュールでインターフェースオブジェクトではなく単一の値をエクスポートするために行われます。

生成されたラッパー関数の引数としてrequireexportsmoduleを定義し(呼び出し時に適切な値を渡すことにより)、ローダーはこれらのバインディングがモジュールのスコープ内で使用可能であることを保証します。

requireに渡された文字列が実際のファイル名またはWebアドレスに変換される方法は、システムによって異なります。"./"または"../"で始まる場合、一般的に現在のモジュールのファイル名からの相対パスとして解釈されます。そのため、"./format-date"は、同じディレクトリにあるformat-date.jsというファイルになります。

名前が相対パスでない場合、Node.jsはその名前でインストールされたパッケージを探します。この章のサンプルコードでは、そのような名前はNPMパッケージを参照するものとして解釈します。NPMモジュールのインストールと使用方法の詳細については、第20章で説明します。

独自のINIファイルパーサーを作成する代わりに、NPMのパーサーを使用できるようになりました。

const {parse} = require("ini");

console.log(parse("x = 10\ny = 20"));
// → {x: "10", y: "20"}

ECMAScriptモジュール

CommonJSモジュールは非常にうまく機能し、NPMと組み合わせることで、JavaScriptコミュニティは大規模なコードの共有を開始することができました。

しかし、それらは依然として場当たり的な対応策です。表記法はややぎこちないです。—例えば、exportsに追加したものはローカルスコープでは使用できません。また、requireは文字列リテラルだけでなく、あらゆる種類の引数を取る通常の関数呼び出しであるため、コードを実行せずにモジュールの依存関係を判断することは困難です。

そのため、2015年からのJavaScript標準では、独自の異なるモジュールシステムが導入されています。これは通常、ESモジュールと呼ばれ、ESはECMAScriptの略です。依存関係とインターフェースの主要な概念は変わりませんが、詳細は異なります。一つには、表記法が言語に統合されました。依存関係にアクセスするために関数を呼び出す代わりに、特別なimportキーワードを使用します。

import ordinal from "ordinal";
import {days, months} from "date-names";

export function formatDate(date, format) { /* ... */ }

同様に、exportキーワードは、ものをエクスポートするために使用されます。関数、クラス、またはバインディング定義(letconst、またはvar)の前に付けることができます。

ESモジュールのインターフェースは単一の値ではなく、名前付きバインディングのセットです。上記のモジュールは、formatDateを関数にバインドします。別のモジュールからインポートする場合、値ではなく*バインディング*をインポートします。つまり、エクスポートするモジュールはいつでもバインディングの値を変更でき、それをインポートするモジュールはその新しい値を参照します。

defaultという名前のバインディングがある場合、それはモジュールの主要なエクスポート値として扱われます。例のordinalのようなモジュールを、バインディング名の周りに中括弧を付けずにインポートすると、その`default`バインディングが取得されます。このようなモジュールは、`default`エクスポートに加えて、他のバインディングを異なる名前でエクスポートすることもできます。

デフォルトのエクスポートを作成するには、式、関数宣言、またはクラス宣言の前に`export default`と書きます。

export default ["Winter", "Spring", "Summer", "Autumn"];

`as`という単語を使用して、インポートされたバインディングの名前を変更することができます。

import {days as dayNames} from "date-names";

console.log(dayNames.length);
// → 7

もう1つの重要な違いは、ESモジュールのインポートは、モジュールのスクリプトの実行開始前に行われることです。つまり、`import`宣言は関数やブロック内には現れず、依存関係の名前は任意の式ではなく、引用符で囲まれた文字列でなければなりません。

執筆時点では、JavaScriptコミュニティはこのモジュールスタイルを採用しているところです。しかし、それは遅いプロセスでした。フォーマットが指定されてから、ブラウザとNode.jsがそれをサポートし始めるまでに数年かかりました。そして、現在ではほとんどサポートされていますが、このサポートにはまだ問題があり、このようなモジュールをNPMを通じてどのように配布すべきかについての議論はまだ続いています。

多くのプロジェクトはESモジュールを使用して書かれ、公開時に自動的に他のフォーマットに変換されます。私たちは2つの異なるモジュールシステムが並行して使用されている過渡期にあり、どちらのシステムでもコードを読み書きできることは役に立ちます。

ビルドとバンドル

実際、多くのJavaScriptプロジェクトは、厳密にはJavaScriptで書かれていません。第8章で述べた型チェック方言など、広く使用されている拡張機能があります。また、実際にJavaScriptを実行するプラットフォームに拡張機能が追加されるずっと前から、計画されている言語拡張機能を使い始めることもよくあります。

これを可能にするために、彼らはコードを*コンパイル*し、選択したJavaScriptの方言からプレーンな古いJavaScriptに翻訳します。—あるいは過去のバージョンのJavaScriptにさえ翻訳します。—古いブラウザでも実行できるようにするためです。

200個の異なるファイルで構成されるモジュール化されたプログラムをWebページに含めると、独自の問題が発生します。ネットワーク経由で1つのファイルを取得するのに50ミリ秒かかる場合、プログラム全体をロードするのに10秒かかります。複数のファイルを同時にロードできる場合は、その半分になることもあります。それは多くの時間の無駄です。1つの大きなファイルを取得する方が、多くの小さなファイルを取得するよりも高速である傾向があるため、Webプログラマーは、Webに公開する前に、(苦労してモジュールに分割した)プログラムを1つの大きなファイルにまとめるツールを使用し始めました。このようなツールは、*バンドラー*と呼ばれます。

さらに、ファイルの数だけでなく、ファイルの*サイズ*もネットワーク経由での転送速度を決定します。そのため、JavaScriptコミュニティは*ミニファイアー*を発明しました。これらは、JavaScriptプログラムを受け取り、コメントと空白を自動的に削除し、バインディングの名前を変更し、コードの一部をスペースの少ない同等のコードに置き換えることで、プログラムを小さくするツールです。

そのため、NPMパッケージにあるコードやWebページで実行されるコードが、*複数*の変換段階を経て、最新のJavaScriptから過去のJavaScriptに変換され、ESモジュール形式からCommonJSに変換され、バンドルされ、ミニファイされていることは珍しくありません。これらのツールは退屈で急速に変化する傾向があるため、本書では詳細には触れません。実行するJavaScriptコードは、多くの場合、記述されたままのコードではないことに注意してください。

モジュール設計

プログラムの構造化は、プログラミングのより微妙な側面の1つです。重要な機能は、さまざまな方法でモデル化できます。

優れたプログラム設計は主観的なものであり、トレードオフや好みの問題が関わってきます。よく構造化された設計の価値を学ぶ最良の方法は、多くのプログラムを読んだり、作業したりして、何がうまくいき、何がうまくいかないかに気づくことです。苦痛を伴う混乱は「そういうものだ」と決めつけないでください。ほとんどすべての構造は、より深く考えることで改善できます。

モジュール設計の一つの側面は使いやすさです。複数の人が使用するもの、あるいは自分自身でさえ、3か月後に自分が何をしたかを具体的に覚えていない場合に使用するものを設計する場合、インターフェースがシンプルで予測可能であれば役立ちます。

それは既存の慣習に従うことを意味する場合があります。良い例は`ini`パッケージです。このモジュールは、`parse`関数と`stringify`関数(INIファイルを書き込むため)を提供することで、標準の`JSON`オブジェクトを模倣しており、`JSON`と同様に、文字列とプレーンオブジェクトを変換します。そのため、インターフェースは小さく、使い慣れたものであり、一度使用すれば、使用方法を覚えている可能性が高くなります。

模倣する標準関数や広く使用されているパッケージがない場合でも、単純なデータ構造を使用し、単一の、焦点を絞った処理を行うことで、モジュールの予測可能性を維持できます。NPMのINIファイル解析モジュールの多くは、例えば、ハードディスクからそのようなファイルを直接読み込んで解析する関数を備えています。これにより、ファイルシステムに直接アクセスできないブラウザでこのようなモジュールを使用することができなくなり、ファイル読み取り関数とモジュールを*組み合わせる*ことでより適切に対処できたはずの複雑さが追加されます。

これは、モジュール設計のもう一つの有用な側面、つまり他のコードと組み合わせやすさを示しています。値を計算する焦点を絞ったモジュールは、副作用のある複雑なアクションを実行する大きなモジュールよりも、幅広いプログラムに適用できます。ディスクからファイルを読み取ることにこだわるINIファイルリーダーは、ファイルの内容が他のソースから来ているシナリオでは役に立ちません。

関連して、ステートフルオブジェクトは有用な場合や必要な場合もありますが、関数でできることは関数を使用してください。NPMのINIファイルリーダーのいくつかは、最初にオブジェクトを作成し、次にファイルを読込み、最後に特殊なメソッドを使用して結果を取得する必要があるインターフェーススタイルを提供しています。この種の手法はオブジェクト指向の伝統では一般的ですが、ひどいものです。単一の関数呼び出しを行って次に進む代わりに、オブジェクトをさまざまな状態に移行させる儀式を実行する必要があります。また、データは特殊なオブジェクト型にラップされているため、それと対話するすべてのコードはその型について知る必要があり、不要な相互依存関係が生じます。

多くの場合、新しいデータ構造の定義は避けられません。言語標準ではいくつかの基本的なデータ構造しか提供されておらず、多くのタイプのデータは配列やマップよりも複雑でなければなりません。しかし、配列で十分な場合は、配列を使用してください。

少し複雑なデータ構造の例として、第7章のグラフがあります。JavaScriptでグラフを表す明白な方法は1つではありません。その章では、プロパティが文字列の配列(そのノードから到達可能な他のノード)を保持するオブジェクトを使用しました。

NPMにはいくつかの異なる経路探索パッケージがありますが、このグラフ形式を使用しているものはありません。通常、グラフのエッジには重み、つまりそれに関連付けられたコストまたは距離を設定できます。これは、私たち的表现では不可能です。

たとえば、`dijkstrajs`パッケージがあります。私たちの`findRoute`関数と非常によく似た、経路探索へのよく知られたアプローチは、最初にそれを書き留めたエドガー・ダイクストラにちなんで、*ダイクストラ法*と呼ばれています。`js`サフィックスは、JavaScriptで書かれていることを示すために、多くの場合パッケージ名に追加されます。この`dijkstrajs`パッケージは、私たちのものと同様のグラフ形式を使用しますが、配列の代わりに、プロパティ値が数値(エッジの重み)であるオブジェクトを使用します。

そのため、そのパッケージを使用したい場合は、グラフが想定どおりの形式で保存されていることを確認する必要があります。簡略化されたモデルでは、各道路のコストを同じ(1ターン)として扱うため、すべてのエッジの重みは同じになります。

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"]

これは、さまざまなパッケージが類似したものを記述するために異なるデータ構造を使用している場合、それらを組み合わせることが困難になるため、構成の障壁となる可能性があります。したがって、構成可能性を考慮して設計する場合は、他の人がどのようなデータ構造を使用しているかを確認し、可能な場合はその例に従ってください。

まとめ

モジュールは、明確なインターフェースと依存関係を持つ部分にコードを分割することにより、より大きなプログラムに構造を提供します。インターフェースは、他のモジュールから見えるモジュールの一部であり、依存関係は、それが使用する他のモジュールです。

JavaScriptは歴史的にモジュールシステムを提供していなかったため、CommonJSシステムはそれを基盤に構築されました。その後、ある時点で*組み込み*システムが導入され、現在、CommonJSシステムと不安定に共存しています。

パッケージは、単独で配布できるコードの塊です。NPMはJavaScriptパッケージのリポジトリです。そこからあらゆる種類の有用な(そして役に立たない)パッケージをダウンロードできます。

演習

モジュール式ロボット

これらは、第7章のプロジェクトが作成するバインディングです。

roads
buildGraph
roadGraph
VillageState
runRobot
randomPick
randomRobot
mailRoute
routeRobot
findRoute
goalOrientedRobot

そのプロジェクトをモジュール式プログラムとして記述する場合、どのモジュールを作成しますか?どのモジュールがどの他のモジュールに依存し、それらのインターフェースはどのようになりますか?

どの部分がNPMであらかじめ作成されている可能性が高いか?NPMパッケージを使用するか、自分で作成するか?

私がやったことは次のとおりです(ただし、繰り返しになりますが、特定のモジュールを設計する*正しい*方法は1つではありません)。

道路グラフを構築するために使用されるコードは、`graph`モジュールにあります。独自の経路探索コードよりもNPMの`dijkstrajs`を使用したいので、`dijkstrajs`が期待する種類のグラフデータを作成します。このモジュールは、単一の関数`buildGraph`をエクスポートします。モジュールが入力形式に依存するのを少なくするために、`buildGraph`はハイフンを含む文字列ではなく、2つの要素の配列を受け入れるようにします。

`roads`モジュールには、未加工の道路データ(`roads`配列)と`roadGraph`バインディングが含まれています。このモジュールは`./graph`に依存し、道路グラフをエクスポートします。

`VillageState`クラスは`state`モジュールにあります。指定された道路が存在することを確認できる必要があるため、`./roads`モジュールに依存しています。また、`randomPick`も必要です。これは3行の関数なので、内部ヘルパー関数として`state`モジュールに入れることができます。しかし、`randomRobot`もそれを必要としています。そのため、それを複製するか、独自のモジュールに入れる必要があります。この関数はNPMの`random-item`パッケージに存在するため、良い解決策は、両方のモジュールをそれに依存させることです。`runRobot`関数は状態管理に密接に関連している小さな関数であるため、このモジュールにも追加できます。モジュールは`VillageState`クラスと`runRobot`関数の両方をエクスポートします。

最後に、ロボットは、`mailRoute`などの依存する値とともに、`example-robots`モジュールに入れることができます。これは、`./roads`に依存し、ロボット関数をエクスポートします。`goalOrientedRobot`が経路探索を実行できるようにするために、このモジュールは`dijkstrajs`にも依存しています。

NPMモジュールにいくつかの作業をオフロードすることで、コードは少し小さくなりました。個々のモジュールは、かなり単純なことを行い、単独で読むことができます。コードをモジュールに分割すると、プログラムの設計をさらに改善できることがよくあります。この場合、`VillageState`とロボットが特定の道路グラフに依存しているのは少し奇妙に思えます。グラフを状態のコンストラクターの引数にして、ロボットに状態オブジェクトからグラフを読み取らせる方が良いかもしれません。これにより、依存関係が減り(これは常に良いことです)、異なるマップでシミュレーションを実行できるようになります(これはさらに優れています)。

自分で書くことができたことにNPMモジュールを使用するのは良い考えでしょうか?原則として、そうです。経路探索関数のような重要なことについては、自分で作成するときに間違いを犯したり、時間を無駄にしたりする可能性があります。`random-item`のような小さな関数の場合、自分で作成するのは簡単です。ただし、必要な場所にそれらを追加すると、モジュールが乱雑になる傾向があります。

しかし、適切なNPMパッケージを見つける作業を過小評価すべきではありません。たとえ見つかったとしても、うまく動作しなかったり、必要な機能が不足している可能性があります。さらに、NPMパッケージに依存する場合、それらがインストールされていることを確認し、プログラムと一緒に配布し、定期的にアップグレードする必要がある場合があります。

繰り返しますが、これはトレードオフであり、パッケージがどれだけ役立つかによってどちらの方法を選択することもできます。

道路モジュール

第7章の例に基づいて、道路の配列を含み、それらを表すグラフデータ構造をroadGraphとしてエクスポートするCommonJSモジュールを作成してください。これは、グラフを構築するために使用される関数buildGraphをエクスポートするモジュール./graphに依存する必要があります。この関数は、2つの要素を持つ配列(道路の始点と終点)の配列を想定しています。

// Add dependencies and 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"
];

これはCommonJSモジュールであるため、グラフモジュールをインポートするにはrequireを使用する必要があります。これはbuildGraph関数をエクスポートするものとして説明されており、分割代入構文を用いた`const`宣言でインターフェースオブジェクトから取り出すことができます。

roadGraphをエクスポートするには、exportsオブジェクトにプロパティを追加します。 buildGraphroadsと正確に一致しないデータ構造をとるため、道路文字列の分割はモジュール内で行う必要があります。

循環依存関係

循環依存関係とは、モジュールAがBに依存し、Bも直接的または間接的にAに依存する状況です。多くのモジュールシステムは、このようなモジュールを読み込む順序を選択しても、各モジュールの依存関係が実行前に読み込まれていることを確認できないため、これを単純に禁止しています。

CommonJSモジュールは、制限された形式の循環依存関係を許可します。モジュールがデフォルトのexportsオブジェクトを置き換えず、ロードが完了するまで互いのインターフェースにアクセスしない限り、循環依存関係は問題ありません。

本章の前半で示したrequire関数は、このタイプの依存関係サイクルをサポートしています。どのようにサイクルを処理するのかわかりますか?サイクル内のモジュールがデフォルトのexportsオブジェクトを置き換えた場合、何が問題になるでしょうか?

秘訣は、requireがモジュールのロードを開始する前に、モジュールをキャッシュに追加することです。そうすれば、実行中にrequire呼び出しがロードを試みても、既に認識されており、モジュールをもう一度ロードし始めるのではなく(最終的にスタックオーバーフローが発生します)、現在のインターフェースが返されます。

モジュールがmodule.exports値を上書きすると、ロードが完了する前にインターフェース値を受け取った他のモジュールは、意図したインターフェース値ではなく、デフォルトのインターフェースオブジェクト(おそらく空)を取得します。