19 March 2012
ページ ツール |
Basic JavaScript programming knowledge.
すべて
この記事は、JavaScriptでよく使用されるデザインパターンに関するシリーズ記事の第1部です。デザインパターンはプログラミングにおける実証済みの手法であり、特に、大規模なJavaScriptアプリケーションを大きなグループで作成する場合に不可欠なコードの保守性、スケーラビリティ、分離性を向上させます。
このシリーズ記事の第2部では、さらに、アダプター、デコレーター、ファクトリという3つのデザインパターンを紹介します。第3部では、さらに、プロキシ、オブザーバー、コマンドという3つのデザインパターンを紹介します。
シングルトンパターンは、オブジェクトのインスタンスが必ず1つしか作られないようにしたい場合に使用します。従来のオブジェクト指向プログラミング言語では、静的なプロパティとメソッド、および動的なプロパティとメソッドの両方が存在するクラスが関係するため、シングルトン作成の概念は少々複雑であり、理解しづらいものでした。しかし、本稿で取り上げるのはJavaScriptであり、これは厳密なクラスを持たない動的な言語であるため、JavaScriptのシングルトンは非常にシンプルです。
実装の詳細を解説する前に、シングルトンパターンをアプリケーションで使用すると便利な理由を説明します。オブジェクトのインスタンスを確実に1つに制限できるということは、非常に実用的です。サーバー側言語では、データベース接続の処理にシングルトンを使用することもできます。要求ごとにデータベース接続を繰り返し作成するのは、リソースの無駄でしかないからです。同様に、フロントエンドJavaScriptでは、すべてのAJAX要求を処理するオブジェクトをシングルトンにすることもできます。シングルトン化のルールは、まったく同じ機能を持つ新規インスタンスをいくつも作成するのならシングルトン化する、というシンプルなものです。
とはいえ、シングルトン化の理由はこれだけではありません。少なくともJavaScriptでは、シングルトンを使用することで名前空間のオブジェクトと関数を整理できるので、グローバル名前空間が無秩序になりません。グローバル名前空間が乱雑になるのは、サードパーティコードを使用している場合は特に、避けたい事態です。名前空間の定義にシングルトンを使用することを、モジュールデザインパターンとも言います。
シングルトンの作成にあたり実際に必要な作業は、オブジェクトリテラルの作成だけです。
var Singleton = {
prop: 1,
another_prop: 'value',
method: function() {…},
another_method: function() {…}
};
非公開のプロパティとメソッドを持つシングルトンを作成することもできますが、その場合はクロージャおよび自己実行型匿名関数を使用するため、作業が少し複雑になります。関数内では、いくつかのローカル関数と変数が宣言されます。次に、オブジェクトリテラルを作成して返します。このリテラルには、より大きな関数スコープ内で宣言した変数と関数を参照するメソッドをいくつか含みます。関数宣言の直後に()を挿入しておくと、外部関数が直ちに実行され、返されたオブジェクトリテラルが変数に割り当てられます。この説明がわかりづらいようでしたら、次のコードに目を通してください。説明は、コードの後に続きます。
var Singleton = (function() {
var private_property = 0,
private_method = function () {
console.log('This is private');
}
return {
prop: 1,
another_prop: 'value',
method: function() {
private_method();
return private_property;
},
another_method: function() {…}
}
}());
関数内で先頭にvarを付けて宣言されている変数は、その関数内からのみアクセス可能です。さらに、アクセスできるのはその関数内で宣言されている関数群(例えば、オブジェクトリテラル内の関数群など)のみです。returnステートメントにより、外部関数の自己実行後にシングルトンに割り当てられるオブジェクトリテラルが返されます。
JavaScriptでは、名前空間を定義するには、オブジェクトを別のオブジェクトのプロパティとして追加します。つまり、1層以上の階層構造になります。これはコードを論理セクションごとに分類する場合に便利です。YUI JavaScriptライブラリには名前空間に多数の階層が含まれ、少々過剰な感がありますが、一般には名前空間の入れ子構造は2~3階層以内に抑えることがベストプラクティスと考えられています。次のコードは、名前空間の定義の例です。
var Namespace = {
Util: {
util_method1: function() {…},
util_method2: function() {…}
},
Ajax: {
ajax_method: function() {…}
},
some_method: function() {…}
};
// Here's what it looks like when it's used
Namespace.Util.util_method1();
Namespace.Ajax.ajax_method();
Namespace.some_method();
前述のとおり、名前空間の定義を使用することで、グローバル変数の数を最小限に抑えることができます。それどころか、もしその方がよければ、アプリケーション全体をappという名前のシングルトンオブジェクト名前空間にアタッチすることもできます。シングルトンデザインパターンおよび名前空間の定義での応用についてもっと知りたい場合は、筆者の個人ブログの「JavaScript Design Patterns: Singleton」に説明がありますので、ぜひご覧ください。
シングルトンパターンの説明を読んで、「簡単だなぁ」と感じた方もどうかご心配なく。次は、もう少し複雑なパターンを2つ説明します。まず1つはコンポジットパターンです。コンポジットは、その名のとおり、1つのエンティテイを形成する複数のパーツで構成されるオブジェクトのことです。この1つのエンティテイは、すべてのパーツのアクセスポイントとして機能します。これを使うと、コードが非常にシンプルになりますが、コンポジットに含まれるパーツの数を把握する間接的な方法がないため、当てにならない面もあります。
コンポジットの説明には図解が一番です。図1には、2つのタイプのオブジェクトがあります。コンテナとギャラリーはコンポジット、画像はリーフ(葉)です。コンポジットは子を持つことができますが、通常はあまり多くの挙動を実装しません。リーフには挙動の大半が含まれますが、少なくとも、従来のコンポジットの例では子を持つことはできません。
もう1つ、現実にあるコンポジットパターンで、皆さんがそうとは知らずに使っていたと思われる例をご紹介しましょう。コンピューターのファイル構造は、コンポジットパターンの一例です。例えば、フォルダーを削除すると、その中身もすべて削除されますが、これは本質的にコンポジットパターンの動作そのものです。ツリー構造の上部のコンポジットオブジェクトに対してメソッドを呼び出すことができ、メッセージは階層の下方へ伝達されます。
この例では、コンポジットパターンのサンプルとして画像ギャラリーを作成します。アルバム、ギャラリー、画像という3レベルの階層構造になります。図1の構成で言うと、アルバムとギャラリーはコンポジットで、画像はリーフに相当します。この構造は、コンポジットに必要とされるよりも明確な構造ですが、この例では、階層をコンポジットまたはリーフのみに制限した方が理にかなっています。標準的なコンポジットでは、リーフにできる階層レベルについて制限はありませんし、レベル数の制限もありません。
手始めに、アルバムとギャラリーの両方に使用されるGalleryComposite
という「クラス」を作成します。DOM操作にはjQueryを使用してコードを簡素にしていることに着目してください。
var GalleryComposite = function (heading, id) {
this.children = [];
this.element = $('<div id="' + id + '" class="composite-gallery"></div>')
.append('<h2>' + heading + '</h2>');
}
GalleryComposite.prototype = {
add: function (child) {
this.children.push(child);
this.element.append(child.getElement());
},
remove: function (child) {
for (var node, i = 0; node = this.getChild(i); i++) {
if (node == child) {
this.children.splice(i, 1);
this.element.detach(child.getElement());
return true;
}
if (node.remove(child)) {
return true;
}
}
return false;
},
getChild: function (i) {
return this.children[i];
},
hide: function () {
for (var node, i = 0; node = this.getChild(i); i++) {
node.hide();
}
this.element.hide(0);
},
show: function () {
for (var node, i = 0; node = this.getChild(i); i++) {
node.show();
}
this.element.show(0);
},
getElement: function () {
return this.element;
}
}
このコード例は長いので、ここで簡単に説明しておきましょう。add
、remove
、getChild
の各メソッドが、コンポジットの構築に使用されています。この例では、実際にはremove
およびgetChild
は使用されませんが、この2つのメソッドは動的なコンポジットの作成に役立ちます。hide
、show
、getElement
の各メソッドは、DOMの操作に使用されます。このコンポジットは、ユーザーに表示されるページ上のギャラリーの表現として設計されています。コンポジットは、hide
とshow
を介してギャラリー要素をコントロールできます。アルバムに対してhide
を呼び出すと、アルバム全体が非表示になります。1枚の画像に対してのみ呼び出した場合は、その画像だけが非表示になります。
次に、GalleryImage
クラスを作成します。このクラスでは、GalleryComposite
とまったく同じメソッドを使用することに着目してください。つまり、どちらのクラスも同じインターフェイスを実装します。ただし、画像はリーフであり、子を持つことはできないため、GalleryImageクラスは子に関するメソッドに対しては実際には何もしません。コンポジットを機能させるには、同じインターフェイスを使用する必要があります。コンポジット要素は、追加しようとする要素が別のコンポジット要素であるかリーフであるかを把握していないので、子に対してこれらのメソッドを呼び出そうとしたときにエラーにならないよう正常に機能させる必要があるからです。
var GalleryImage = function (src, id) {
this.children = [];
this.element = $('<img />')
.attr('id', id)
.attr('src', src);
}
GalleryImage.prototype = {
// Due to this being a leaf, it doesn't use these methods,
// but must implement them to count as implementing the
// Composite interface
add: function () { },
remove: function () { },
getChild: function () { },
hide: function () {
this.element.hide(0);
},
show: function () {
this.element.show(0);
},
getElement: function () {
return this.element;
}
}
以上で、オブジェクトのプロトタイプを作成できたので使ってみましょう。以下は、画像ギャラリーを実際にビルドするコードです。
var container = new GalleryComposite('', 'allgalleries');
var gallery1 = new GalleryComposite('Gallery 1', 'gallery1');
var gallery2 = new GalleryComposite('Gallery 2', 'gallery2');
var image1 = new GalleryImage('image1.jpg', 'img1');
var image2 = new GalleryImage('image2.jpg', 'img2');
var image3 = new GalleryImage('image3.jpg', 'img3');
var image4 = new GalleryImage('image4.jpg', 'img4');
gallery1.add(image1);
gallery1.add(image2);
gallery2.add(image3);
gallery2.add(image4);
container.add(gallery1);
container.add(gallery2);
// Make sure to add the top container to the body,
// otherwise it'll never show up.
container.getElement().appendTo('body');
container.show();
以上が、コンポジットに関する説明です。この画像ギャラリーの実際のデモは、筆者のブログのデモページでご覧いただけます。また、筆者のブログの「JavaScript Design Patterns: Composite」という記事では、このパターンに関してもう少し詳しく説明しています。
ファサードパターンは、この記事で紹介する最後のデザインパターンです。このパターンは関数やその他のコード片から成り、複雑なインターフェイスをシンプルにします。ファサードパターンは実際によく使用されており、大半の関数はこの目的で作られていると言えるでしょう。ファサードの目的は、大型のロジックを簡素化し、1つのシンプルな関数呼び出しにまとめることです。
デザインパターンを使用しているとはまったく自覚せずに、実は既にファサードパターンをずっと使っているかもしれません。あらゆるプログラミング言語で使用されるライブラリはどれも皆、程度の差はあれ、ファサードパターンを使用しています。一般的に、このパターンは複雑なものをシンプルにできるからです。
例として、jQueryを見てみましょう。jQueryには、jquery()
という、DOMの照会や要素の作成、DOM要素のjQueryオブジェクトへの変換など、多岐にわたる動作をする1つの関数があります。DOMの照会を調べて、この機能を作成するのに使用されたコードの行数にさっと目を通してみてください。きっと、「このコードを自分で書かないで済んでよかった」とほっとすることでしょう。それだけ長く複雑なコードなのです。ここでは
façadeパターンをうまく使って、数百行ものコードを短い関数1つに変換しています。
ファサードは非常にわかりやすいパターンですが、もし興味があれば、筆者の個人ブログの「JavaScript Design Patterns: Façade」の記事内により詳しい説明があります。
JavaScriptデザインパターンシリーズの第1部では、シングルトン、コンポジット、ファサードについて取り上げました。このシリーズの第2部では、さらにアダプター、デコレーター、ファクトリという3つのデザインパターンについて説明します。第3部では、さらに、プロキシ、オブザーバー、コマンドという3つのデザインパターンを紹介します。
また、筆者の個人ブログでもJavaScriptデザインパターンに関するシリーズ投稿を書き上げており、そこではこのシリーズ記事で取り上げていないパターンについてもいくつか紹介しています。シリーズ記事の投稿はすべて筆者のブログでお読みいただけます。
この作品はCreative Commons Attribution-Noncommercial-Share Alike 3.0 Unported Licenseに基づき使用が許可されます。この作品に含まれるサンプルコードに関して、このライセンスの範囲を超えた使用の許可については、アドビのWebサイトを参照してください。