モジュール

簡単に削除できるコードを書きましょう。拡張しやすいコードではなく。

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

理想的には、プログラムは明確で分かりやすい構造を持っています。その動作は簡単に説明でき、各部分は明確に定義された役割を果たします。

実際には、プログラムは有機的に成長します。プログラマーが新しいニーズを認識するにつれて、機能の一部が追加されます。このようなプログラムの構造を維持するには、継続的な注意と作業が必要です。これは将来、つまりプログラムを次に処理する人が得られるメリットですが、それを無視してプログラムのさまざまな部分が深く絡み合うことを許してしまう誘惑があります。

これにより、2つの実際的な問題が発生します。第一に、絡み合ったシステムを理解することは困難です。すべてが互いに影響し合う場合、特定の部分を個別に検討することが困難になります。全体的な理解を構築せざるを得ません。第二に、このようなプログラムの機能を別の状況で使用したい場合、コンテキストから分離しようとするよりも、書き直す方が簡単かもしれません。

このような大きく構造化されていないプログラムには、「巨大な泥団子 (big ball of mud)」という表現がよく使われます。すべてがくっついていて、一部を取り出そうとすると、全体がばらばらになり、結果として混乱を招くだけです。

モジュール化されたプログラム

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

モジュールインターフェースは、第6章で見たオブジェクトインターフェースと多くの共通点があります。これらはモジュールの一部を外部世界に公開し、残りをプライベートに保ちます。

しかし、モジュールが他のモジュールが使用するために提供するインターフェースは、物語の半分に過ぎません。優れたモジュールシステムには、モジュールが他のモジュールからどのコードを使用するかを指定することも必要です。これらの関係は依存関係と呼ばれます。モジュールAがモジュールBの機能を使用する場合、そのモジュールに依存しているとされます。これらがモジュール自体に明確に指定されている場合、特定のモジュールを使用するために存在する必要がある他のモジュールを特定し、依存関係を自動的にロードするために使用できます。

モジュールがお互いにやり取りする方法が明示的であれば、システムは、部品が明確に定義されたコネクタを介して相互作用するLEGOのようになり、すべてが混ざり合う泥のような状態ではなくなります。

ESモジュール

元のJavaScript言語には、モジュールの概念がありませんでした。すべてのスクリプトは同じスコープで実行され、別のスクリプトで定義された関数にアクセスするには、そのスクリプトによって作成されたグローバルバインディングを参照する必要がありました。これは、コードの意図しない、分かりにくい絡み合いを積極的に促進し、関連のないスクリプトが同じバインディング名を使用しようとするなどの問題を招きました。

ECMAScript 2015以降、JavaScriptは2種類のプログラムをサポートしています。スクリプトは従来の方法で動作します。そのバインディングはグローバルスコープで定義され、他のスクリプトを直接参照する方法はありません。モジュールは独自の個別のスコープを取得し、スクリプトでは使用できないimportexportキーワードをサポートして、依存関係とインターフェースを宣言します。このモジュールシステムは通常、ESモジュール(ここでESはECMAScriptを表します)と呼ばれます。

モジュール化されたプログラムは、このような多くのモジュールで構成され、それらのインポートとエクスポートを介して接続されています。

次の例では、曜日名と数値(DategetDayメソッドによって返される)を変換するモジュールを示します。インターフェースの一部ではない定数と、インターフェースの一部である2つの関数を定義しています。依存関係はありません。

const names = ["Sunday", "Monday", "Tuesday", "Wednesday",
               "Thursday", "Friday", "Saturday"];

export function dayName(number) {
  return names[number];
}
export function dayNumber(name) {
  return names.indexOf(name);
}

exportキーワードは、関数、クラス、またはバインディング定義の前に置くことで、そのバインディングがモジュールのインターフェースの一部であることを示します。これにより、他のモジュールはインポートすることでそのバインディングを使用できます。

import {dayName} from "./dayname.js";
let now = new Date();
console.log(`Today is ${dayName(now.getDay())}`);
// → Today is Monday

importキーワードに続いて、中括弧で囲まれたバインディング名のリストを記述することで、別のモジュールのバインディングを現在のモジュールで使用できます。モジュールは引用符で囲まれた文字列で識別されます。

このようなモジュール名が実際のプログラムにどのように解決されるかは、プラットフォームによって異なります。ブラウザはそれらをWebアドレスとして扱い、Node.jsはそれらをファイルに解決します。モジュールを実行すると、依存する他のすべてのモジュール(およびそれらが依存するモジュール)がロードされ、エクスポートされたバインディングはそれらをインポートするモジュールで使用できるようになります。

インポートとエクスポートの宣言は、関数、ループ、またはその他のブロック内には配置できません。モジュールがロードされると、モジュール内のコードがどのように実行されるかに関係なく、すぐに解決されます。これを反映するために、それらは外部モジュール本体にのみ表示する必要があります。

したがって、モジュールのインターフェースは、モジュールに依存する他のモジュールがアクセスできる名前付きバインディングのコレクションで構成されます。インポートされたバインディングは、名前の後にasを使用して、新しいローカル名を付けることができます。

import {dayName as nomDeJour} from "./dayname.js";
console.log(nomDeJour(3));
// → Wednesday

モジュールには、単一のバインディングのみをエクスポートするモジュールによく使用されるdefaultという特別なエクスポートも含まれる場合があります。デフォルトのエクスポートを定義するには、式、関数宣言、またはクラス宣言の前にexport defaultと書きます。

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

このようなバインディングは、インポートの名前を中括弧で囲まずにインポートされます。

import seasonNames from "./seasonname.js";

モジュールからすべてのバインディングを同時にインポートするには、import *を使用できます。名前を提供すると、その名前はモジュールのすべてのエクスポートを含むオブジェクトにバインドされます。これは、多くの異なるエクスポートを使用する場合に役立ちます。

import * as dayName from "./dayname.js";
console.log(dayName.dayName(3));
// → Wednesday

パッケージ

別々の部品からプログラムを構築し、それらの部品の一部を個別に実行できることの利点の1つは、同じ部品を異なるプログラムで使用できることです。

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

コードの複製を開始すると、コピーを移動して最新の状態に保つことに時間とエネルギーを無駄にすることにすぐに気づきます。そこでパッケージが登場します。パッケージとは、配布(コピーとインストール)できるコードの塊です。1つ以上のモジュールを含んでおり、他のどのパッケージに依存しているかについての情報が含まれています。パッケージには通常、それ自体が何をするのかを説明するドキュメントも付属しているので、作成者以外の人も使用できます。

パッケージで問題が見つかったり、新しい機能が追加されたりすると、パッケージが更新されます。それに依存するプログラム(これもパッケージである可能性があります)は、新しいバージョンをコピーしてコードに加えられた改善を取得できます。

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

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

執筆時点では、NPMで300万を超える異なるパッケージが利用可能です。正直なところ、その多くはがらくたです。しかし、ほぼすべての有用で公開されているJavaScriptパッケージは、NPMにあります。たとえば、第9章で作成したものと同様のINIファイルパーサーは、パッケージ名iniで使用できます。

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

ダウンロード可能な高品質のパッケージを利用できることは非常に価値があります。つまり、100人が以前に書いたプログラムを再発明するのを回避し、数回のキー操作で堅牢で十分にテストされた実装を入手できることがよくあります。

ソフトウェアは複製が容易なため、一度作成されれば、他の人への配布は効率的なプロセスです。しかし、そもそもソフトウェアを作成すること作業であり、コードのバグを発見した人や、新機能を提案する人への対応は、さらに多くの作業を伴います。

デフォルトでは、作成したコードの著作権はあなたに帰属し、他の人はあなたの許可なく使用できません。しかし、親切な人もいること、そして優れたソフトウェアを公開することでプログラマーの間で少し有名になる可能性があることから、多くのパッケージは、他の人による使用を明示的に許可するライセンスの下で公開されています。

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

さて、独自のINIファイルパーサーを作成する代わりに、NPMのものを使用できます。

import {parse} from "ini";

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

CommonJSモジュール

2015年以前、JavaScript言語に組み込みのモジュールシステムがなかった時代でも、人々はすでに大規模なシステムをJavaScriptで構築していました。それを実現可能にするために、彼らはモジュールを必要としていました。

コミュニティは、言語の上に独自の即席モジュールシステムを設計しました。これらは、関数を使用してモジュールのローカルスコープを作成し、通常のオブジェクトを使用してモジュールインターフェースを表します。

当初、人々は単にモジュールのスコープを作成するために、モジュール全体を「すぐに呼び出される関数式」で手動でラップし、インターフェースオブジェクトを単一のグローバル変数に割り当てていました。

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

このスタイルのモジュールはある程度分離を提供しますが、依存関係を宣言しません。代わりに、インターフェースをグローバルスコープに配置し、依存関係があれば同じようにすることを期待します。これは理想的ではありません。

独自のモジュールローダーを実装すれば、より良いことができます。JavaScriptモジュールに追加された最も広く使用されているアプローチは、CommonJSモジュールと呼ばれます。Node.jsは最初からこのモジュールシステムを使用していました(ただし、現在はESモジュールもロードできます)。そして、これはNPMの多くのパッケージで使用されているモジュールシステムです。

CommonJSモジュールは通常のスクリプトのように見えますが、他のモジュールとやり取りするために使用する2つのバインディングにアクセスできます。1つ目はrequireという関数です。依存関係のモジュール名でこれを呼び出すと、モジュールがロードされ、そのインターフェースが返されます。2つ目はexportsというオブジェクトで、モジュールのインターフェースオブジェクトです。これは空の状態から始まり、エクスポートする値を定義するためにプロパティを追加します。

このCommonJSの例モジュールは、日付書式設定関数を提供します。これはNPMの2つのパッケージ—数値を"1st""2nd"のような文字列に変換するordinalと、曜日の英名と月の英名を取得するdate-names—を使用します。これは、Dateオブジェクトとテンプレート文字列を受け取る単一の関数formatDateをエクスポートします。

テンプレート文字列には、西暦を意味するYYYYや、月の通日を表すDoなど、書式を指示するコードを含めることができます。"MMMM Do YYYY"のような文字列を渡すと、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()];
  });
};

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

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

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

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

CommonJSは、モジュールをロードする際に、そのコードを関数でラップし(独自のローカルスコープを与える)、requireexportsのバインディングをその関数に引数として渡すモジュールローダーで実装されています。

ファイル名でファイルを読み取り、その内容を取得するreadFile関数にアクセスできると仮定すると、requireの簡略化された形式を次のように定義できます。

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は、引数のリスト(カンマ区切りの文字列として)と関数本体を含む文字列を受け取り、それらの引数と本体を持つ関数値を返す、JavaScriptの組み込み関数です。これは興味深い概念です—プログラムは文字列データから新しいプログラムの部分を作成できます—しかし、誰かがプログラムをだまして、彼らが提供した文字列をFunctionに挿入させれば、プログラムに何でもさせることができるため、危険でもあります。

標準のJavaScriptにはreadFileのような関数は提供されていませんが、ブラウザやNode.jsなどのさまざまなJavaScript環境は、ファイルにアクセスする独自の手段を提供しています。この例では、readFileが存在すると仮定しています。

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

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

このシステムとESモジュール間の重要な違いは、ESモジュールのインポートはモジュールのスクリプトの実行が開始される前に発生するのに対し、requireは既に実行中のモジュールで呼び出される通常の関数であることです。import宣言とは異なり、require呼び出しは関数の内部に表示されることがあり、依存関係の名前は文字列として評価される任意の式にすることができますが、importはプレーンな引用符で囲まれた文字列のみを許可します。

JavaScriptコミュニティのCommonJSスタイルからESモジュールへの移行は、遅く、やや困難なものでした。幸いなことに、現在ではNPMの人気パッケージのほとんどがコードをESモジュールとして提供しており、Node.jsはESモジュールがCommonJSモジュールからインポートすることを許可しています。CommonJSコードはまだ目にするものですが、このスタイルで新しいプログラムを作成する本当の理由はもはやありません。

ビルドとバンドル

多くのJavaScriptパッケージは、技術的にはJavaScriptで記述されていません。第8章で述べた型チェック方言であるTypeScriptなどの言語拡張は広く使用されています。また、人々は、実際にJavaScriptを実行するプラットフォームに追加されるずっと前から、計画されている新しい言語機能の使用を開始することがよくあります。これを可能にするために、彼らはコードをコンパイルし、選択したJavaScript方言を通常のJavaScript—または過去のバージョンのJavaScript—に変換して、ブラウザで実行できるようにします。

200個もの異なるファイルからなるモジュール式プログラムをウェブページに含めることは、独自の課題を生み出します。ネットワーク経由で単一のファイルの取得に50ミリ秒かかるとすると、プログラム全体のロードには10秒かかります。複数のファイルを同時にロードできる場合でも、その半分程度です。これは多くの無駄な時間です。単一の大きなファイルをフェッチする方が、多くの小さなファイルをフェッチするよりも高速な傾向があるため、ウェブプログラマーは、プログラム(苦労してモジュールに分割したもの)を単一の大きなファイルに結合するツールを使用し始めました。このようなツールは、バンドラーと呼ばれます。

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

NPMパッケージで見つかったり、ウェブページで実行されたりするコードが、複数の変換段階を経ていることは珍しくありません—最新のJavaScriptから過去のJavaScriptへの変換、モジュールの単一ファイルへの結合、コードのミニファイ—。これらのツールは多くあり、人気のあるものが頻繁に変化するため、この本では詳細には触れません。単にそのようなものがあることを認識し、必要に応じて調べてください。

モジュール設計

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

優れたプログラム設計は主観的です—トレードオフがあり、好みの問題もあります。よく構造化された設計の価値を学ぶ最良の方法は、多くのプログラムを読み込んだり、作業したりして、何がうまくいくか、何がうまくいかないかを注意することです。苦痛な混乱が「そういうものだ」と仮定しないでください。より多くの思考を投入することで、ほとんどすべての構造を改善できます。

モジュール設計の1つの側面は、使いやすさです。複数の人—または3か月後には自分が行ったことの具体的な内容を忘れてしまう可能性のある自分自身でさえ—によって使用されることを意図したものを設計する場合は、インターフェースがシンプルで予測可能であることが役立ちます。

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

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

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

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

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

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

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

たとえば、dijkstrajsパッケージがあります。経路探索に対するよく知られたアプローチで、私たちのfindRoute関数と非常によく似ているものは、最初にそれを書き留めたEdsger Dijkstraにちなんでダイクストラアルゴリズムと呼ばれています。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パッケージを使用しますか、それとも自分で作成しますか?

表示ヒント…

私が行ったであろう方法(ただし、特定のモジュールを設計する唯一の正しい方法はありません)

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

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

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

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

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

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

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

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

道路モジュール

第7章の例に基づいて、道路の配列を含み、それらをroadGraphとして表すグラフデータ構造をエクスポートするESモジュールを作成します。これは、グラフの作成に使用される関数buildGraphをエクスポートするモジュール./graph.jsに依存します。この関数は、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"
];
表示ヒント…

これはESモジュールであるため、グラフモジュールにアクセスするにはimportを使用する必要があります。それはbuildGraph関数をエクスポートすると説明されており、デストラクチャリングconst宣言を使用してそのインターフェースオブジェクトから選択できます。

roadGraphをエクスポートするには、その定義の前にキーワードexportを付けます。buildGraphroadsと正確に一致しないデータ構造を受け入れるため、道路文字列の分割はモジュール内で行う必要があります。

循環依存関係

循環依存とは、モジュールAがBに依存し、Bも直接的または間接的にAに依存する状況です。多くのモジュールシステムでは、このようなモジュールのロード順序を問わず、各モジュールの依存関係が実行前にロードされていることを保証できないため、これは禁止されています。

CommonJSモジュールは、限定的な形の循環依存を許可しています。モジュールがロードを完了するまで互いのインターフェースにアクセスしない限り、循環依存は問題ありません。

この章の前の方で説明したrequire関数は、このタイプの依存関係サイクルをサポートしています。それがどのようにサイクルを処理するか分かりますか?

表示ヒント…

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