今回は、例によってVisitorパターンの実例となるものを取り上げるのだが、完全にこのパターンとぴったり重なるものがなかなか見つからなかった。そこで、似たような挙動を実現しているものを紹介しよう。DOMTraversalとそれに関連するクラス群だ。
DOMツリーの「横断」
このクラスは、Cocoaではなくて、HTMLレンダリングエンジンであるWeb Kitフレームワークに含まれるものだ。Web Kitは、Webページ表示のAPIに加えて、DOMツリーにアクセスするためのAPIも備えている。これは、W3Cで標準化されているものだ。標準的なDOM APIの、Objective-Cバージョンとなる訳だ。
このWeb KitのDOM APIは、SDKに含まれる形でのドキュメント化は行われていないようだ。そこで、これらを使いたいときは、フレームワークに含まれるヘッダファイルを直接参照することになる。100以上のファイルが用意されている。
さて、今回これらの中から取り上げるのは、DOMTraversalと呼ばれるものだ。traversalは「横断」といった意味になる。つまり、HTMLページを表すDOMツリーの構造の中を、横断しながら必要な処理を行うためのクラスだ。
まず、DOMTraversalの定義を見てみよう。DOMTraversal.hに記述されている。実はこれ、DOMDocumentのカテゴリという形になっている。
List 1.
@interface DOMDocument (DOMDocumentTraversal)
- (DOMNodeIterator*)createNodeIterator:(DOMNode*)root
whatToShow:(unsigned)whatToShow
filter:(id <DOMNodeFilter>)filter
expandEntityReferences:(BOOL)expandEntityReferences;
- (DOMTreeWalker*)createTreeWalker:(DOMNode*)root
whatToShow:(unsigned)whatToShow
filter:(id <DOMNodeFilter>)filter
expandEntityReferences:(BOOL)expandEntityReferences;
@end
定義されているメソッドは、2つだ。1つはcreateNodeIterator:whatToShow:filter:expandEntityReferences:。DOMNodeIteratorというクラスを作成する。もう1つは、createTreeWalker:whatToShow:filter:expandEntityReferences:。こちらは、DOMTreeWalkerというクラスを作成することになる。
DOMTraversalは、DOMツリーの中のノードを順々に訪問していくのが目的だ。ノードを取り出すために使われるのが、DOMNodeIteratorクラスおよびDOMTreeWalkerクラスになる。
これらのクラスの定義を見てみよう。まず、DOMNodeIteratorクラスだ。
List 2.
@interface DOMNodeIterator : DOMObject
{
id <DOMNodeFilter> m_filter;
}
@property(readonly, retain) DOMNode* root;
@property(readonly) unsigned whatToShow;
@property(readonly, retain) id <DOMNodeFilter> filter;
@property(readonly) BOOL expandEntityReferences;
- (DOMNode*)nextNode;
- (DOMNode*)previousNode;
- (void)detach;
@end
メソッドとして、次のノードを取り出すためのnextNode、前のノードを取り出すためのpreviousNodeがあるのが分かる。これらを使えば、DOMノードを順々に取り出すことが出来るのが分かるだろう。
次に、DOMTreeWalkerの定義を見てみる。
List 3.
@interface DOMTreeWalker : DOMObject
{
id <DOMNodeFilter> m_filter;
}
@property(readonly, retain) DOMNode* root;
@property(readonly) unsigned whatToShow;
@property(readonly, retain) id <DOMNodeFilter> filter;
@property(readonly) BOOL expandEntityReferences;
@property(retain) DOMNode* currentNode;
- (DOMNode*)parentNode;
- (DOMNode*)firstChild;
- (DOMNode*)lastChild;
- (DOMNode*)previousSibling;
- (DOMNode*)nextSibling;
- (DOMNode*)previousNode;
- (DOMNode*)nextNode;
@end
こちらには、現在のノードを指定するためのcurrentNodeプロパティがある。さらに、そこから関連するノードを取得するために、親ノードを取り出すparentNode、子ノードのためのfirstChild、lastChild、兄弟ノードのためのpreviousSibling、nextSibling、さらに順々にアクセスするためのpreviousNode、nextNode、といったメソッドが用意されている。これらを使えば、DOMツリー内部を自由に動き回ることが出来るのが分かるだろう。
ただし、DOMツリーにアクセスしたいだけならば、DOMTraversalを使う必要はない。これの特徴は、自分の興味あるノードだけを取り出す事が出来る事だ。そのために使われるのが、DOMDocumentTraversalで定義されている2つのメソッドで指定する引数、DOMNodeFilterだ。
DOMNodeFilterは、次のようなプロトコルとして定義されている。
List 4.
@protocol DOMNodeFilter <NSObject>
- (short)acceptNode:(DOMNode*)n;
@end
acceptNode:というメソッドが1つ定義されている。このメソッドには、DOMNodeIteratorなどを使って、巡回中のDOMノードが渡されてくる。そのノードを調べて、処理を行うかどうかを決定するのだ。つまり、ノードのフィルターになっている訳だ。
「accept」という単語が付いているところが、Visitorパターンを連想させるだろう。
DOMからリンクを抜き出す
では、これらのクラスを使ってみよう。例として、DOMツリーからリンクを抜き出すコードを紹介する。
まず、DOMNodeFilterプロトコルを使ったクラスを作る。AnchorNodeFilterというクラスにしよう。
List 5.
@interface AnchorNodeFileter : NSObject <DOMNodeFilter>
@end
@implementation AnchorNodeFilter
- (short)acceptNode:(DOMNode*)node
{
// DOMノードのクラスをチェック
if ([node isKindOfClass:[DOMHTMLAnchorElement class]]) {
return DOM_FILTER_ACCEPT;
}
return DOM_FILTER_SKIP;
}
@end
AnchorNodeFilterでは、acceptNode:メソッドを実装している。ここでは、渡されたDOMノードがリンクを表すもの、つまりアンカーであるかどうかをチェックしている。DOMのクラス群では、アンカーはDOMHTMLAnchorElementというクラスになるので、このクラスかどうかをチェックすればいい。返り値として、アンカーノードを処理するにはDOM_FILTER_ACCEPTを、このノードはスキップする場合にはDOM_FILTER_SKIPを返す。
このクラスを使って、リンクを取り出してみよう。
List 6.
// フィルターを作成する
AnchorNodeFilter* filter;
filter = [[AnchorNodeFilter alloc] init];
[filter autorelease];
// DOMドキュメントから、DOMNodeIteratorを作成する
DOMNodeIterator* iterator;
iterator = [domDocument createNodeIterator:[domDocument documentElement]
:DOM_SHOW_ELEMENT :filter :NO];
// アンカーノードを取り出す
DOMNode* node;
while (node = [iterator nextNode]) {
DOMHTMLAnchorElement* anchor;
anchor = (DOMHTMLAnchorElement*)node;
// リンク先のアドレスを取得する
NSString* href;
href = [anchor href];
...
まず、AnchorNodeFilterをインスタンス化する。それを引数として指定して、DOMTraversalのメソッドを呼び出すのだ。ここでは、DOMNodeIteratorを作成する方のメソッドを使っている。このオブジェクトからnextNodeを呼び出す事で、アンカーノードを順々に取得することが出来るのだ。あとは、それに応じた処理を行えばいい。
これで、DOMツリーからリンクを抜き出すことが出来る。
DOMTraversalとVisitorパターン
では、このDOMTraversalとVisitorパターンは、どのような関係にあると言えるだろう。Visitorパターンには、Visitor、Element、Object Structureといった登場人物がいた。これらをDOMTraversalに当てはめてみよう。
まず、ElementはDOMNodeということになるだろう。Visitorは、DOMNodeFilterだろうか。ただし、Visitorパターンでは、VisitorがElementに対する処理を行うことになっている。だがDOMNodeFilterは、DOMNodeに対する処理は行っていない。ここで行っているのはノードのフィルタリングだけであり、処理はクライアント側に記述することになっている。
残りのDOMNodeIteratorとDOMTreeWalkerは、構造を渡り歩くためのクラスとなる。Visitorパターンでは、Object StructureまたはClientということになるだろうか。
このように比較してみると、Visitorパターンでは処理のクラス化に重きが置かれていたのに対して、DOMTraversalでは対象となるノードの取得の仕方に特徴があると言えるだろう。
デザインパターンとCocoa
これで、GoF本に含まれる23のデザインパターンのすべてを紹介した。いかがであったろうか。
デザインパターンとCocoaフレームワークを比較し続けてきた訳だが、最後に筆者なりの感想を述べたい。両者の間には考え方の差違がいくつかあったが、それはクラス化の粒度にあると思う。デザインパターンでは、とても細かいクラス分割が行われている。それはカプセル化や再利用促進といった、オブジェクト指向の利点を追求するがためだろう。また学術よりの本なので、一般性を追い求めたがためとも言える。
それに対して、Cocoaは過度のクラス化を行わないように設定されている。象徴的なのが、デリゲートとデータソースの考え方だろう。これらをプロトコルとして定義する事により、サブクラス化を行わなくとも機能を次々と追加することが出来るのだ。また、ターゲット/アクションの考え方も、不必要なサブクラス化を避けるのに一役買っているだろう。あくまで実践的な、今まさに現場で使われているフレームワークとしては、クラス化の粒度が細かくなってパフォーマンスが落ちたり管理が大変になるのを避けたかったのではないだろうか。
最後に
さて、デザインパターンの解説が無事に終わったのを受けて、このダイナミックObjective-Cの連載を終了とさせていただきたいと思う。終了の理由は、Objective-Cについては、もう充分にその魅力を解説出来たと思うからだ。Objective-Cが持つ「動的」という側面は、120回を超える連載で徹底的に暴かれたと思う。
また、連載開始当初とは、Objective-Cを取り巻く環境も変わった。何といっても、Objective-Cが使われるプラットフォームとして、Mac OS Xに加えてiPhoneが登場した事が大きい。これからは言語だけではなく、それが使われるプラットフォームやフレームワークにもっと目を向けていく必要があるだろう。
このような訳で、Objective-Cに関する連載は終了という決断にいたった。読者の方々の、長年のご愛顧に感謝したい。次の機会にお会い出来る事を楽しみにしている。