第9章: モジュール性
¶ この章では、プログラムを整理するプロセスについて説明します。小さなプログラムでは、整理はほとんど問題になりません。しかし、プログラムが大きくなると、その構造と解釈を把握するのが困難になるサイズに達する可能性があります。簡単に、そのようなプログラムはスパゲッティのボウルのように見え始め、すべてが他のすべてに接続されているように見える無定形の塊になります。
¶ プログラムを構造化するとき、私たちは2つのことを行います。プログラムを、それぞれ特定の役割を持つモジュールと呼ばれる小さなパーツに分割し、これらのパーツ間の関係を指定します。
¶ 第8章で、テラリウムを開発中に、第6章で説明したいくつかの関数を使用しました。この章では、clone
やDictionary
型など、テラリウムとは特に関係のないいくつかの新しい概念も定義しました。これらすべてが、環境に無作為に追加されました。このプログラムをモジュールに分割する1つの方法は次のようになります。
FunctionalTools
モジュール。これには第6章の関数が含まれており、何も依存していません。- 次に、
ObjectTools
。これにはclone
やcreate
のようなものが含まれており、FunctionalTools
に依存します。 Dictionary
。ディクショナリ型を含み、FunctionalTools
に依存します。- 最後に、
Terrarium
モジュール。これはObjectTools
とDictionary
に依存します。
¶ モジュールが別のモジュールに依存する場合、そのモジュールの関数または変数を使用し、このモジュールがロードされた場合にのみ機能します。
¶ 依存関係が円を形成しないようにすることが重要です。循環依存は実際的な問題(モジュールA
とB
が互いに依存している場合、どちらを最初にロードする必要がありますか?)を引き起こすだけでなく、モジュール間の関係をより複雑にし、前に述べたスパゲッティのモジュール化されたバージョンになる可能性があります。
¶ ほとんどの最新のプログラミング言語には、何らかのモジュールシステムが組み込まれています。JavaScriptにはありません。繰り返しになりますが、私たちは何かを自分で発明する必要があります。最も明らかな方法は、すべてのモジュールを異なるファイルに入れることです。これにより、どのコードがどのモジュールに属するかが明確になります。
¶ ブラウザは、ウェブページのHTMLでsrc
属性を持つ<script>
タグを見つけたときに、JavaScriptファイルをロードします。拡張子.js
は通常、JavaScriptコードを含むファイルに使用されます。コンソールでは、ファイルをロードするためのショートカットは、load
関数によって提供されます。
load("FunctionalTools.js");
¶ 場合によっては、間違った順序でloadコマンドを実行すると、エラーが発生します。モジュールがDictionary
オブジェクトを作成しようとしたが、Dictionary
モジュールがまだロードされていない場合、コンストラクターが見つからず、失敗します。
¶ これを解決するのは簡単だと思うかもしれません。モジュールのファイルの先頭にload
の呼び出しをいくつか記述して、依存するすべてのモジュールをロードするだけです。残念ながら、ブラウザの動作方法により、load
を呼び出しても、指定されたファイルがすぐにロードされるわけではありません。ファイルは、現在のファイルの実行が完了した後にロードされます。通常は、手遅れです。
¶ ほとんどの場合、実用的な解決策は、依存関係を手動で管理することです。HTMLドキュメントにscript
タグを正しい順序で配置します。
¶ 依存関係の管理を(部分的に)自動化する方法は2つあります。1つ目は、モジュール間の依存関係に関する情報を別のファイルに保持することです。これは最初にロードでき、ファイルのロード順序を決定するために使用できます。2つ目の方法は、script
タグ(load
は内部的にこのようなタグを作成して追加します)を使用せず、ファイルのコンテンツを直接フェッチ(第14章を参照)してから、eval
関数を使用して実行することです。これにより、スクリプトのロードが瞬時になり、処理が容易になります。
¶ eval
は、「evaluate」の略で、興味深い関数です。文字列値を指定すると、文字列の内容がJavaScriptコードとして実行されます。
eval("print(\"I am a string inside a string!\");");
¶ eval
を使用して面白いことができるのは想像できます。コードは新しいコードを構築し、実行できます。しかし、ほとんどの場合、eval
の創造的な使い方で解決できる問題は、匿名関数の創造的な使い方でも解決できます。後者の方が、奇妙な問題を引き起こす可能性が低くなります。
¶ 関数内でeval
が呼び出されると、すべての新しい変数はその関数に対してローカルになります。したがって、load
のバリエーションが内部でeval
を使用する場合、Dictionary
モジュールをロードすると、load
関数内にDictionary
コンストラクターが作成されます。これは、関数が戻るとすぐに失われます。これを回避する方法はありますが、かなり扱いにくいです。
¶ 依存関係管理の最初のバリアントについて簡単に説明しましょう。依存関係情報用の特別なファイルが必要であり、次のようなものになります。
var dependencies = {"ObjectTools.js": ["FunctionalTools.js"], "Dictionary.js": ["ObjectTools.js"], "TestModule.js": ["FunctionalTools.js", "Dictionary.js"]};
¶ dependencies
オブジェクトには、他のファイルに依存する各ファイルのプロパティが含まれています。プロパティの値は、ファイル名の配列です。Dictionary
モジュールがまだロードされていない可能性があるため、ここではDictionary
オブジェクトを使用できなかったことに注意してください。このオブジェクトのすべてのプロパティは".js"
で終わるため、__proto__
やhasOwnProperty
のような隠しプロパティと干渉する可能性は低く、通常のオブジェクトで問題ありません。
¶ 依存関係マネージャーは、2つのことを行う必要があります。まず、ファイルの依存関係をファイル自体より前にロードすることにより、ファイルが正しい順序でロードされるようにする必要があります。次に、ファイルが2回ロードされないようにする必要があります。同じファイルを2回ロードすると問題が発生する可能性があり、間違いなく時間の無駄です。
var loadedFiles = {}; function require(file) { if (dependencies[file]) { var files = dependencies[file]; for (var i = 0; i < files.length; i++) require(files[i]); } if (!loadedFiles[file]) { loadedFiles[file] = true; load(file); } }
¶ require
関数を使用して、ファイルとそのすべての依存関係をロードできるようになりました。依存関係(およびその依存関係の可能性)を処理するために、自身を再帰的に呼び出す方法に注目してください。
require("TestModule.js");
test();
¶ プログラムを小さく、優れたモジュールのセットとして構築することは、プログラムが多くの異なるファイルを使用することを意味することがよくあります。Webプログラミングでは、ページ上に小さなJavaScriptファイルがたくさんあると、ページのロードが遅くなる傾向があります。ただし、これは問題である必要はありません。プログラムを多数の小さなファイルとして記述してテストし、プログラムをWebに「公開」するときに、すべてを1つの大きなファイルに入れることができます。
¶ オブジェクト型と同様に、モジュールにはインターフェイスがあります。FunctionalTools
のような単純な関数コレクションモジュールでは、インターフェイスは通常、モジュールで定義されているすべての関数で構成されます。それ以外の場合、モジュールのインターフェイスは、モジュール内で定義された関数のごく一部にすぎません。たとえば、第6章の原稿からHTMLへのシステムには、単一の関数renderFile
のインターフェイスのみが必要です。(HTMLを構築するためのサブシステムは別のモジュールになります。)
¶ Dictionary
など、単一のオブジェクト型のみを定義するモジュールの場合、オブジェクトのインターフェイスはモジュールのインターフェイスと同じです。
¶ JavaScriptでは、「トップレベル」の変数はすべて1つの場所にまとめて存在します。ブラウザでは、この場所はwindow
という名前で見つけることができるオブジェクトです。名前は少し奇妙で、environment
またはtop
の方が意味をなしていたでしょうが、ブラウザはJavaScript環境をウィンドウ(または「フレーム」)に関連付けているため、window
が論理的な名前であると誰かが判断しました。
show(window); show(window.print == print); show(window.window.window.window.window);
¶ 3行目が示すように、名前window
はこの環境オブジェクトの単なるプロパティであり、それ自体を指しています。
¶ 多くのコードが環境にロードされると、多くのトップレベルの変数名が使用されます。追跡できるコードよりも多くのコードがあると、すでに他のことに使用されていた名前を誤って使用することが非常に簡単になります。これにより、元の値を使用していたコードが破損します。トップレベルの変数の増加は名前空間の汚染と呼ばれ、JavaScriptでは非常に深刻な問題になる可能性があります。言語は、既存の変数を再定義しても警告しません。
¶ この問題を完全に取り除く方法はありませんが、可能な限り汚染を少なくするように注意することで、大幅に減らすことができます。1つには、モジュールは、外部インターフェイスの一部ではない値にトップレベルの変数を使用しないでください。
¶ もちろん、モジュールで内部関数や変数をまったく定義できないのは、あまり実用的ではありません。幸いなことに、これを回避するトリックがあります。モジュールのすべてのコードを関数内に記述し、最後にモジュールのインターフェイスの一部である変数をwindow
オブジェクトに追加します。これらは同じ親関数で作成されたため、モジュールのすべての関数は互いに表示できますが、モジュールの外部のコードは表示できません。
function buildMonthNameModule() { var names = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; function getMonthName(number) { return names[number]; } function getMonthNumber(name) { for (var number = 0; number < names.length; number++) { if (names[number] == name) return number; } } window.getMonthName = getMonthName; window.getMonthNumber = getMonthNumber; } buildMonthNameModule(); show(getMonthName(11));
¶ これは、月名とその数値(Date
で使用される、1月が 0
となるもの)を変換する非常にシンプルなモジュールを構築します。ただし、buildMonthNameModule
はモジュールのインターフェースの一部ではないトップレベル変数であることに注意してください。また、インターフェース関数の名前を3回繰り返す必要があります。うーん。
¶ 最初の問題は、モジュール関数を匿名にして、直接呼び出すことで解決できます。これを行うには、関数値の周りに一対の括弧を追加する必要があります。そうしないと、JavaScriptはそれを直接呼び出すことができない通常の関数定義と見なします。
¶ 2番目の問題は、window
オブジェクトにエクスポートする必要がある値を含むオブジェクトを渡すことができるヘルパー関数 provide
で解決できます。
function provide(values) { forEachIn(values, function(name, value) { window[name] = value; }); }
¶ これを使用して、次のようなモジュールを作成できます。
(function() { var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; provide({ getDayName: function(number) { return names[number]; }, getDayNumber: function(name) { for (var number = 0; number < names.length; number++) { if (names[number] == name) return number; } } }); })(); show(getDayNumber("Wednesday"));
¶ 最初からこのようなモジュールを作成することはお勧めしません。コードを作成している間は、これまで使用してきたシンプルなアプローチを使用して、すべてをトップレベルに配置する方が簡単です。そうすることで、ブラウザでモジュールの内部値を調べたり、テストしたりできます。モジュールがほぼ完成したら、関数でラップするのは難しくありません。
¶ モジュールが非常に多くの変数をエクスポートするため、それらすべてをトップレベル環境に入れるのは良くない場合があります。このような場合、標準の Math
オブジェクトが行うように、モジュールを、プロパティがエクスポートする関数と値である単一のオブジェクトとして表現できます。例:
var HTML = { tag: function(name, content, properties) { return {name: name, properties: properties, content: content}; }, link: function(target, text) { return HTML.tag("a", [text], {href: target}); } /* ... many more HTML-producing functions ... */ };
¶ このようなモジュールの内容が非常に頻繁に必要なため、常に HTML
を入力するのが面倒な場合は、provide
を使用してトップレベル環境に移動できます。
provide(HTML); show(link("http://download.oracle.com/docs/cd/E19957-01/816-6408-10/object.htm", "This is how objects work."));
¶ モジュールの内部変数を関数内に配置し、この関数が外部インターフェースを含むオブジェクトを返すことで、関数アプローチとオブジェクトアプローチを組み合わせることもできます。
¶ Array
や Object
などの標準プロトタイプにメソッドを追加する場合、名前空間の汚染に似た問題が発生します。2つのモジュールが Array.prototype
に map
メソッドを追加することにした場合、問題が発生する可能性があります。これらの2つのバージョンの map
がまったく同じ効果を持つ場合、事態は運が良ければ引き続き機能しますが。
¶ モジュールまたはオブジェクトタイプのインターフェースを設計することは、プログラミングの微妙な側面の1つです。一方では、あまりにも多くの詳細を公開したくありません。モジュールを使用するときに邪魔になるだけです。他方では、単純すぎたり一般的すぎたりすることも望ましくありません。複雑な状況や特殊な状況でモジュールを使用できなくなる可能性があるからです。
¶ 場合によっては、複雑なものには詳細な「ローレベル」インターフェース、簡単な状況にはシンプルな「ハイレベル」インターフェースという、2つのインターフェースを提供することが解決策となります。2番目のインターフェースは通常、最初のインターフェースが提供するツールを使用して非常に簡単に構築できます。
¶ その他のケースでは、インターフェースの基礎となる適切なアイデアを見つける必要があります。これを、8章で見たさまざまな継承アプローチと比較してください。コンストラクターではなくプロトタイプを中心的な概念にすることで、いくつかのことを大幅に簡単にすることができました。
¶ 優れたインターフェース設計の価値を学ぶための最良の方法は、残念ながら、悪いインターフェースを使用することです。それらにうんざりしたら、それらを改善する方法を理解し、その過程で多くのことを学びます。ひどいインターフェースが「単にそういうものだ」と決めつけないようにしてください。それを修正するか、より優れた新しいインターフェースでラップしてください(12章でこの例を示します)。
¶ 多くの引数を必要とする関数があります。場合によっては、これは単に設計が不十分であり、いくつかのより控えめな関数に分割することで簡単に修正できます。しかし、他のケースでは、回避する方法はありません。通常、これらの引数のいくつかは、賢明な「デフォルト」値を持ちます。たとえば、range
のさらに拡張されたバージョンを記述できます。
function range(start, end, stepSize, length) { if (stepSize == undefined) stepSize = 1; if (end == undefined) end = start + stepSize * (length - 1); var result = []; for (; start <= end; start += stepSize) result.push(start); return result; } show(range(0, undefined, 4, 5));
¶ どの引数がどこに行くかを覚えるのが難しくなる可能性があり、length
引数が使用されている場合に2番目の引数として undefined
を渡す必要があるという煩わしさも言うまでもありません。引数をオブジェクトでラップすることにより、この関数への引数の渡し方をより包括的にすることができます。
function defaultTo(object, values) { forEachIn(values, function(name, value) { if (!object.hasOwnProperty(name)) object[name] = value; }); } function range(args) { defaultTo(args, {start: 0, stepSize: 1}); if (args.end == undefined) args.end = args.start + args.stepSize * (args.length - 1); var result = []; for (; args.start <= args.end; args.start += args.stepSize) result.push(args.start); return result; } show(range({stepSize: 4, length: 5}));
¶ defaultTo
関数は、オブジェクトにデフォルト値を追加するのに役立ちます。2番目の引数のプロパティを最初の引数にコピーし、すでに値を持っているプロパティはスキップします。
¶ 複数のプログラムで役立つ可能性のあるモジュールまたはモジュールのグループは、通常、ライブラリと呼ばれます。多くのプログラミング言語では、非常に多くの高品質なライブラリが利用可能です。これは、プログラマーが常に最初から始める必要がないことを意味し、生産性を大幅に向上させることができます。残念ながら、JavaScriptの場合、利用可能なライブラリの量はそれほど多くありません。
¶ しかし最近、これは改善されているようです。map
や clone
のような「基本的な」ツールを備えた優れたライブラリが数多くあります。他の言語では、組み込みの標準機能としてそのような明らかに便利なものを提供する傾向がありますが、JavaScriptでは、自分でそれらのコレクションを作成するか、ライブラリを使用する必要があります。ライブラリを使用することをお勧めします。作業が少なく、ライブラリ内のコードは通常、自分で記述したものよりも徹底的にテストされています。
¶ これらの基本をカバーするものとして、(とりわけ)「軽量」ライブラリの prototype、mootools、jQuery、および MochiKit があります。また、一連の基本ツールを提供するだけでなく、多くのことを行う、より大きな「フレームワーク」もいくつか利用可能です。YUI(Yahooによる)および Dojo が、このジャンルで最も人気のあるもののようです。これらはすべて無料でダウンロードして使用できます。私の個人的なお気に入りはMochiKitですが、これは主に好みの問題です。JavaScriptプログラミングに真剣に取り組む場合は、これらの各ドキュメントに目を通し、それらの動作方法と提供するものについての一般的なアイデアを把握することをお勧めします。
¶ 基本的なツールキットが、非自明なJavaScriptプログラムにとってほぼ不可欠であるという事実と、非常に多くの異なるツールキットが存在するという事実が組み合わさって、ライブラリ作成者にとって少しジレンマが生じます。ツールキットの1つにライブラリを依存させるか、基本的なツールを自分で作成してライブラリに含める必要があります。最初のオプションでは、別のツールキットを使用している人がライブラリを使用するのが難しくなり、2番目のオプションでは、ライブラリに多くの非必須コードが追加されます。このジレンマが、優れた、広く使用されているJavaScriptライブラリが比較的少ない理由の1つである可能性があります。将来的には、ECMAScriptの新しいバージョンとブラウザの変更により、ツールキットの必要性が低下し、したがって(部分的に)この問題が解決される可能性があります。