具体例で説明するデザインパターン
デザインパターンってなんだかたいそうなものに考えられがちだけど、実は、そこかしこのライブラリや仕様に取り込まれていることが多い。
「デザインパターン分からん」とか「で、どうやって使うのよ」と悶々としている人には、「どういうところで使わているのか」を知っておけばイメージも沸きやすいし、意外にたいしたことないんだな、ということが分かるんじゃないだろうか。
そこで、JavaScript やその周辺の技術で、デザインパターンがどのように使われているかを紹介してみることにする。
ここでは、GoF本の順番に沿って、以下の11個のパターンを取り上げる。
- 生成に関するパターン
- 構造に関するパターン
- 振る舞いに関するパターン
それでは、レッツゴー!
Factory Method
(追記) 訂正: この説明は factory method については述べていますが、Factory Method パターンには該当しませんでした。申し訳ございません。ブックマークコメントで指摘いただいた id:HiromitsuTakagi 様、ありがとうございます。
ざっくり説明
new を使わずにインスタンス生成をメソッドで行う。パラメータによって生成するものを変えることも多い。
具体例
HTML DOM の createElement() が該当する。
// table タグを作成 var table = document.createElement("table"); table.rows; // 空の配列 // div タグを作成 var div = document.createElement('div'); div.rows; // undefined
第一引数に与えたタグ名によって、異なる HTML ノードが作成されているのが分かる。
DOM 仕様で言えば、createElement("table") では HTMLTableElement オブジェクトが、createElement("div") では HTMLDivElement オブジェクトが返されている。
まとめ
- インスタンス化するためのメソッドを定義する
- DOM の document.createElement() は factory メソッドである
Abstract Factory
ざっくり説明
オブジェクトを作成するためのインターフェースを集約する。どの Factory を渡すかによって、異なるインスタンス群を生成できるようになる。
具体例
HTML DOM と XML DOM の document には共通の Factory メソッド createElement() や createTextNode を備えている。
これを利用して次のような create() 関数を作ってみた。
function create(factory,nodeName){ return factory.createElement(nodeName); } var html_div = create(document,"div"); console.log(html_div instanceof HTMLDivElement); // true var xmldoc = document.implementation.createDocument("", "",null); var xml_div = create(xmldoc,"div"); console.log(xml_div instanceof HTMLDivElement); // false
create() 関数は、渡される Factory がどのようなものかは知らないが、createElement() メソッドを実行すればインスタンス生成できることを知っている。
そのため、HTML DOM の document を渡されたときは、HTML 用のタグを作成するし、XML DOM の document を渡されたときは、XML 用のノードを作成する。
まとめ
- Factory を取り替えることで、作成するオブジェクト群を変えられるようにしたのが Abstract Factory
Builder
ざっくり説明
オブジェクトを作成して組み立てていくためのライブラリを提供する。既存の組み立て方では複雑になる場合や、将来変わる可能性があるときに威力を発揮する。
具体例
DOM ツリーを作成する手順は煩雑だしソースも読みにくい。
var div = document.createElement("div"); var p = document.createElement("p"); p.className = "myParagraph"; var txt = document.createTextNode("foo"); p.appendChild(txt); div.appendChild(div); document.body.appendChild(div);
だけども、innerHTML を使えば一発。
var div = document.createElement("div"); div.innerHTML = "<p class='myParagraph'>foo</p>"; document.body.appendChild(div);
innerHTML はある意味 DOM ノードの Builder であると言ってよいだろう。
jQuery のノード生成の機能、Builder みたいなもんだ。簡単に DOM ツリーを作れる。
$("<div>") .append($("<p>") .attr("class","myParagraph") .text("foo") ) .appendTo("body");
まとめ
- 複雑なものを簡単に作れるようにしておく
- 作るための手順が変わったときにも、Builder さえ変更すればよいので依存性が小さくなる
- DOM ノードの Builder として innerHTML や jQuery が挙げられる
Prototype
ざっくり説明
原型からコピーしてオブジェクトを作成する。
具体例
JavaScript の prototype オブジェクトが近い。ただ、デザパタの Prototype パターンとは少し違うので注意すべし。
デザパタの Prototype パターンは、原型(prototype)からコピーすることでインスタンス生成する。clone() といったほうがイメージが近いかもしれない。
function prototypeGoF(proto){ var obj = {}; for(var name in proto){ obj[name] = proto[name]; } return obj; } var base = {"prop" :"foo"}; var a = prototypeGoF(base); // 複製されている a.prop; // "foo" // 原型 base を変更しても、a の値は変わらない base.prop = "bar"; a.prop // "foo"
それに対して、JavaScript の prototype では、prototype オブジェクトが参照として渡される。あとから prototype を変更しても反映される。
function A(){} A.prototype.prop = "foo"; var a = new A(); a.prop; // "foo" A.prototype.prop = "bar"; a.prop; // "bar"
まとめ
- 原型からオブジェクトを作る
- JavaScript の言語仕様に取り込まれているけど、Prototype パターンがコピーなのに対して、JavaScript の prototype は参照渡しなので注意してね
Singleton
ざっくり説明
オブジェクト指向でグローバル変数っぽいものを実現したいときに使う。
具体例
JavaScript ではグローバル空間汚染できまくりなので、いまさら例える必要はないんじゃないかな。
とはいえ、Singleton にはインスタンスが1つしかないことを保証する、という意味合いもある。これを JavaScript で実現するには次のようにするのが定石になっている。
(function(){ window.$ = function(){ // ... }; }());
無名関数で隠して、グローバル領域に書き込んでる。JavaScript のいくつかのライブラリで実際に使われている手法。
まとめ
- グローバル変数を使いにくい or 使えない言語でグローバル変数を実現するためのもの。
Adapter
ざっくり説明
クラスのインターフェースを変える。別名 Wrapper。
具体例
IE では addEventListener がなくて困るので定義してあげる。
if (!window.addEventListener){ window.addEventListener = function(type,fn){ attachEvent("on" + type,function(){fn.call(this,window.event);}); }; Element.prototype.addEventListener = window.addEventListener; }
これで IE でも addEventListener が使えるようになった。
ただ、デザインパターンの流儀では、サブクラスを定義して元のクラスを隠蔽するのが基本。ここでは、JavaScript 流のやり方で、直接インターフェースを変えてるので、Adapter パターンと言い切るのは少し苦しいかもしれない。
まとめ
- クラスのインターフェースを変えるときに使う
- 既存のオブジェクトを変更できないときに使う
- JavaScript はビルトインのオブジェクトも変更できちゃうので、直接的に書いちゃうよね。
Composite
ざっくり説明
階層構造を表現するためのオブジェクト構造。
具体例
DOM ノード。
addChild()、removeChild() などのメソッドで追加・削除できる。childNodes プロパティで子ノードを参照する。parentNode プロパティもあるね。
まとめ
- 階層構造を表現する
- DOM ノードは Composite パターン使ってるね。階層構造だもんね。
Chain Of Responsibility
ざっくり説明
要求を表すオブジェクトが順番に伝わっていく仕組み。オブジェクトがチェーン状に伝わっていき、それぞれの責任範囲で実装を行える。
具体例
DOM イベントでは、イベントオブジェクトが対象のオブジェクトから親ノードへ順番に伝わっていく。
例えば、この例では、div タグでクリックされると、イベントが div タグに到達して「div clicked!!」と表示される。そのあと、body タグにイベントが到達し、「body clicked!!」と表示される。div と body でそれぞれ独立に同じイベントを処理できるようになっている。
<body> <div id="example">example</div> <script> var div = document.getElementById("example"); div.addEventListener("click",function(event){ alert("div clicked!!"); },false); document.body.addEventListener("click",function(){ alert("body clicked!!"); },false); </script> </body>
もう一例挙げておこう。プロトタイプチェーンという言葉にもあるとおり、prototype は Chain Of Responsibility パターンだ。例えば、arr という配列に対して、arr.myProperty を参照すると、
- a.myProperty
- Array.prototype.myProperty
- Object.prototype.myProperty
の順番に myProperty プロパティを探す。
まとめ
- 順番に情報が伝わっていく
- DOM イベントのバブリングフェーズでは、イベントオブジェクトが順番に伝わっていく
- プロトタイプチェーンもそう。
Iterator
ざっくり説明
オブジェクトを順番にアクセスするための方法を提供する。
具体例
JavaScript ではあんまりイテレータは使わないんだけど、DOM ノードをイテレータ的に使ってみよう。
var p = document.body.firstChild; while(p){ myFunc(p); p = p.nextSibling; }
nextSibling は同じ階層の次の要素を取得するプロパティ。nextSibling を使えば、ノード自身をイテレータのように扱える。
実際、jQuery のソースコード中には
for ( ; n; n = n.nextSibling ) if ( n.nodeType == 1 ) { // 省略 }
というようなコードがある。カウンタ変数 i を使ったほうが読みやすい気もするが、この辺は好みだね。
まとめ
- 順番にアクセスする
- nextSibling プロパティを使えば、DOM ノードをイテレータ的に使える
Observer
ざっくり説明
オブジェクトの状態が変わったときに、依存するオブジェクトに自動的に知らせるための仕組み。
具体例
DOM イベントは Observer パターンそのもの。
addEventListener を使えば、イベント発生したときに呼ばれる関数を登録できる。
elm.addEventListener("click",function(){ // その1 },false); elm.addEventListener("click",function(){ // その2 },false);
このように、1つの対象(Subject)に対して複数のイベントリスナ(Observer)を登録できるところがポイント。イベントが発生すると、全部のイベントリスナが呼ばれる。
あんまり使わないけど、removeEventListener で登録解除できる。また、イベント発生させるときには、dispatchEvent() メソッドを利用できる。
まとめ
- 通知を複数の対象に知らせるための仕組み
- DOM イベントの仕組みが Observer パターンそのまんまなんだよ。
Visitor
ざっくり説明
既存のオブジェクト構造を、外部から操作するための仕組みを追加する。
具体例
SAX(Simple API for XML)。
Java のソースだけど、Simple API for XML - Wikipedia より引用。
import java.io.IOException; import javax.xml.parsers.*; import org.xml.sax.*; import org.xml.sax.helpers.DefaultHandler; public class Test { public static void main(String[] args) throws ParserConfigurationException,SAXException,IOException { SAXParserFactory factory = SAXParserFactory.newInstance(); SAXParser parser = factory.newSAXParser(); parser.parse("http://example.com/rss",new DefaultHandler() { private String text = ""; private boolean isItemStarted = false; public void startElement(String uri,String localName,String qName,Attributes attributes) { if(qName.equals("item")) { isItemStarted = true; } } public void endElement(String uri,String localName,String qName) { if (isItemStarted && qName.equals("title")) { System.out.println(text); } } public void characters(char[] ch,int start,int length) { text = new String(ch,start,length); } }); } }
SAX でパース処理を開始すると、XML のノードを上から順番に見ていき、ノードの内容に応じて、Visitor(ここでは、DefaultHandler) の startElement(),endElement(),characters() などのメソッドを呼び出していく。
まとめ
- SAX はライブラリの構造として Visitor パターンを取り入れている
あとがき
以上、長くなっちゃったけど、なんとなくイメージが伝われば幸いです。
デザパタについて詳しく知りたい場合は デザインパターン (ソフトウェア) - Wikipedia や、定番の書籍を参照あれ。
- 作者: エリック ガンマ
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 1999-10
- メディア: 単行本
- Amazon のレビューを見る
- 作者: 結城 浩
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 2004-06-19
- メディア: 大型本
- Amazon のレビューを見る
パターン指向リファクタリング入門~ソフトウエア設計を改善する27の作法
- 作者: ジョシュア・ケリーエブスキー
- 出版社/メーカー: 日経BP社
- 発売日: 2005-08-04
- メディア: 単行本
- Amazon のレビューを見る
この特集で取り上げられなかったパターンは以下の12個。
- Bridge、Decorator、Facade、Flyweight、Proxy、Command、Interpreter、Mediator、Memento、State、Strategy、Template Method。
JavaScript 界隈で使われているところが思いあたらなかった。JavaScript 以外だったらよく使うものもあるんだけど…。この辺で使ってるのに!というのがあったら教えてほしいです。