|
注)以下の内容は「オブジェクト指向における再利用のためのデザインパターン 改訂版」(ソフトバンク パブリッシング 株式会社発行 著者:Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides 監訳者:本位田 真一、吉田 和樹)からの引用を多分に含んでいます。 C++ でプログラミングをする上でのレベルアップには欠かせない名著です。 興味を持たれた方は是非ご購入をお勧めします。 アマゾン.comで購入される方はここからどうぞ。 |
|
Iteratorパターン | |||||||||||||||
|
内部の構成を公開せずに、その要素に順にアクセスする方法を提供する。 | |||||||||||||||
|
[動機] | |||||||||||||||
|
・リストのような集約オブジェクトはその内部構造を明かすことなく、要素に アクセスする方法を提供すべき。 ・異なる走査ごとにオペレーション(関数)を用意して、Listクラスの インターフェイスをふくらませることは望ましくない。 ・異なる走査自体が必要となるかどうか判断ができないこともある。 | |||||||||||||||
|
[いつ使うか] | |||||||||||||||
|
・集約オブジェクトの内部構造を公開せずに、その中にあるオブジェクトに アクセスしたい場合。 ・集約オブジェクトに対して複数の走査をサポートしたい場合。 ・異なる集約構造の走査に対して、単一のインターフェイスを提供したい場合。 (すなわち、ポリモルフィックなiterationをサポートしたい場合。) | |||||||||||||||
|
[構成要素] | |||||||||||||||
|
Iteratorクラス 要素にアクセスしたり、要素を走査したりするためのインターフェイスを定義する。 ConcreteIteratorクラス …… Concrete:具体的な Iteratorクラスで定義したインターフェイスを実装する。 Aggregateオブジェクト(集合オブジェクト)の走査の際に、カレント要素を記録する。 Aggregateクラス …… Aggregate:集合の Iteratorオブジェクトを生成するためのインターフェイスを定義する。 ConcreteAggregateクラス Aggretageクラスで定義したインターフェイスに対して、 適切なConcreteIteratorクラスのインスタンスを生成して返すように実装する。 (ConcreteAggregateクラスのインスタンスを総称してaggregateと呼ぶ。) | |||||||||||||||
|
[構造] | |||||||||||||||
| |||||||||||||||
|
[協調関係] | |||||||||||||||
|
・ConcreteIterator ConcreteIteratorはaggregateの走査の際に、カレント要素を記録し 走査先を計算することが出来る。 | |||||||||||||||
|
[結果] | |||||||||||||||
|
長所 | |||||||||||||||
|
・aggrageteに対してさまざまな走査がサポートできる。 Iteratorにより走査のアルゴリズム変更が容易になる。変更のためには 単にiteratorを別のものに置き換えるだけで良い。 新しい走査をサポートしたいときはIteratorのサブクラスを定義すればよい。 ・IteratorクラスによりAggregateクラスのインターフェイスは簡単なものになる。 Aggregateクラスが走査のインターフェイスを備えておく必要がなくなる。 ・一つのaggregateに対して複数の走査を(同時に)実行できる。 | |||||||||||||||
|
(欠点) | |||||||||||||||
|
[実装] | |||||||||||||||
|
Iteratorパターンには多くの実装方法とその変形がある。 1. どのオブジェクトがiterationを制御するか。 クライアントがiterationを制御するとき、そのiteratorを外部iteratorと呼ぶ。 またiteratorがiterationを制御するとき、そのiteratorを内部iteratorと呼ぶ。 ・外部iterator 外部iteratorを使うクライアントは、走査を先に進め、iteratorに対して 次の要素を明示的に要求しなければならない。 外部iteratorは内部iteratorより柔軟である。2つの集合が等しいことを 外部iteratorを使って比べるのは容易だが、内部iteratorでは実用的には 不可能である。 ・内部iterator 内部iteratorを使う場合は、クライアントはiteratorに実行すべき オペレーションを渡して、iteratorがそのオペレーションをaggregateの 各要素に対して適用する。 また内部iteratorは無名関数やclosureやcontinuationを提供していない C++のような言語(Small talkやCLOSは提供している)には不向き。 しかし、ユーザーのためにiteratorのロジックを定義している内部iterator の方が容易に使うことができる。 2. どのオブジェクトが走査のアルゴリズムを定義するか。 iteratorは単にiterationの状態を保存しておくためだけに使われることがある。 このようなiteratorをcursorと呼ぶ。 もしiteratorが走査のアルゴリズムを与えているのならば、同じaggrementに 対して異なったiterationアルゴリズムを使うことは容易になる。 また異なったaggrementに対して同じアルゴリズムを再利用することは さらに容易になる。 3. iteratorはどれだけ頑強か。 走査中にaggregateに変更を加えるのは危険である。 robust iteratorはiteratorをaggregateに登録しておき、挿入や削除の際には aggregateは自らが生成したiteratorの内部状態をそれに適合させたり、 正しく走査が行われるように情報を内部で保管しておく。 4. Iteratorのオペレーション追加について。 Iteratorクラスの最小のインターフェイスはオペレーションFirst, Next, IsDone, CurrentItemからなる。 さらにいくつかのオペレーションを追加することで便利になる事がある。 順序づけされたaggregateにはiteratorが指している要素を一つ前に戻す Previousオペレーションを持たせることができる。 整列された、またはインデックス付けされた集合に対してSkipToオペレーションは 便利かもしれない。 5. C++でポリモルフィックなiteratorを使う場合。 これを使うにはfactory methodにより、iteratorが動的にヒープ上に確保されるように なっている必要がある。 ポリモルフィズムが必要でなければ、iteratorはスタック上に確保されるようにして使う。 ポリモルフィックなiteratorはクライアントが削除する責任を持たなければならない。 Proxyパターンはこれの対処策を与える。スタック上に確保したproxyを実際の iteratorに対する窓口として使う。proxyのデストラクタ内でiteratorを 削除する。したがってproxyがスコープの外に出るときに実際のiteratorも 解放されることになる。proxyオブジェクトは例外処理の場合でも メモリを確実に解放することを保証する。 6. iteratorには特権的なアクセス権を持たせても良い。 iteratorをaggregateの拡張と見なすことができ、両者は密接に結びついている。 この密接な関係をfriendクラスとして表現することができる。 これによりiteratorの走査を効率よく実装するためのオペレーションを Aggregateクラスに定義する必要がなくなる。 しかし、特権的なアクセス権は新しい走査を定義するのを困難にする。 別のフレンドクラスを追加することになり、aggregateのインターフェイスを 変えなければならないためである。 この問題を避けるためにaggregateの重要だが公開されていないメンバに アクセスするために、Iteratorクラスに保護的なオペレーションを 入れておくこともできる。Iteratorのサブクラスはaggregateに対して特権的な アクセス権を得るためにこのオペレーションを使う。 (Iteratorのサブクラスだけの特徴) 7. compositeオブジェクトのためのiterator。 Compositeパターンのような再帰的な集約構造上で外部iteratorを 実装するのは難しい。 構造内のカレント要素の位置がネスト化された構造の多階層に及ぶため、 走査を記録するためには構造内でのパスを記録しておかなければならない。 内部iteratorではそれ自体を再帰的に呼び出してパスを呼び出しのスタック中に 暗黙のうちにたくわえることで、カレント要素の位置を 記録しておくことができる。 8. NullIteratorとは。 NullIteratorは境界条件を扱う為に役立つ、変化したIteratorである。 NullIteratorでは常に走査が完了しており、IsDoneオペレーションの戻り値は 常にtrueである。 NullIteratorはCompositeのような木構造のaggregateを走査するのを容易にする。 走査中の各時点でカレントの要素に子ノードに対するiteratorを依頼していく。 aggregateの要素は通常は普通のiteratorを返すが、末端の要素はNullIteratorクラスの インスタンスを返す。 こうすることで構造全体を統一的な方法で走査するように実装できる。 | |||||||||||||||
|
サンプルコード | |||||||||||||||
|
ここでは非常によく使われるListクラスとそのIteratorクラスを サンプルコードとして紹介する。 まず登場するクラスについて説明する。 Employeeクラス … employee:使用人、従業員 ・リストで管理されるクラス Listクラス Iteratorクラス PrintEmployeesオペレーション ・Listに格納されたEmployeeをIteratorを用いて順次表示する関数 以上が今回サンプルとして登場する主要クラスと主要関数である。 PrintEmployeesオペレーションの実装は以下のようになる。 void PrintEmployees(Iterator<Employee*>& i) { for(i.First(); !i.IsDone(); i.Next()) { i.CurrentItem()->Print(); } } 走査のアルゴリズムはiteratorに内包され、順次出力という主要な目的を 達成する関数の内部には含まれていない。 その為前から後ろへ向かう走査の為のiteratorと、後ろから前へ向かう走査のための iteratorを差し替えるだけで、PrintEmployeesは全く変更を加えることなく、 全く異なる走査を実装できる。 List<Employee*>* p_employees; //... ListIterator<Employee*> forward(employees); ReverseListIterator<Employee*> backward(employees); PrintEmployees(forward); // 前から後ろへ出力 PrintEmployees(backward); // 後ろから前へ出力 リストの特定の実装にiteratorが依存しないようにすれば、様々なリストに対する 柔軟性が高まる。 そこで、リストの異なる実装に対してインターフェイスを統一するために AbstractListクラスを導入する。 ポリモルフィックなiterationを可能にするためにAbstractListクラスでは factory methodとしてCreateIteratorオペレーションを定義する。 template<class Item> class AbstractList { public: virtual Iterator<Item>* CreateIterator() const = 0; // ... }; ポリモルフィックなiteratorを利用する場合それが確実に削除されるように することが重要である。 iteratorに対してproxyとして振る舞うIteratorPtrクラスを与える。それは スコープから外れるときにiteratorを一掃する。 IteratorPtrではそのオブジェクトをあたかもiteratorへのポインタのように 扱えるようにoperator->とoperator*の両方をオーバーロードする。 またIteratorPtrのメンバをインラインで実行することで実行時の オーバーヘッドをなくす。 template<class Item> class IteratorPtr { public: IteratorPtr(Iterator<Item>* i): mp_i(i){} ~IteratorPtr(){ delete m_i; } Iterator<Item>* operator->() {return mp_i;} Iterator<Item>& operator*() {return *mp_i} private: // コピーを許さず、割り当てを避けるため // コピーコンストラクタ及び代入演算子をプライベート宣言する IteratorPtr(const IteratorPtr&); IteratorPtr& operator=(const IteratorPtr&); private: Iterator<Item>* mp_i; }; IteratorPtrにより出力の為のコードは簡単になる。 AbstractList<Employee*>* employees; // ... IteratorPtr<Employee*> Iterator(employees->CreateIterator()); PrintEmployees(*iterator); 内部のiteratorでは各要素に対して実行したいオペレーションでiteratorを パラメータ化する方法がある。このような目的のための無名関数やclosureを 提供している言語もあるがC++にはそのようなサポートはない。 そこで (1)(グローバルまたは静的な)関数に対するポインタを渡す。 (2) サブクラス化に頼る。 という二つの選択肢のどちらかを利用する。 (1)の場合、iteratorはiterationの各時点で渡されるオペレーションを呼び出す。 (2)の場合、iteratorはサブクラスが特定の振る舞いを実行するために、 オーバーライドするオペレーションを呼び出す。 しばしばiterationの最中に状態を蓄積しておきたくなるが(1)のような関数では、 静的変数を必要とされるこのような要求に応えることはできない。 (2)ならばこの問題は解決できるが、1つ1つの走査のためにサブクラスを 作っていくのはさらに大変な作業になる。 |