はじめに
@nqounetです。
シリーズ「本棚アプリで覚える集合体の巡回」の第5回、最終回です。
これまでの振り返り
このシリーズでは、本棚アプリを題材に、集合体を巡回する仕組みを段階的に作ってきました。
- 第1回 —
BookクラスとBookShelfクラスを作成し、集合体の基本を学んだ - 第2回 —
forループで全ての本を表示し、カプセル化の問題に直面した - 第3回 —
Moo::RoleでBookIteratorRoleを定義し、BookShelfIteratorクラスを作成した - 第4回 —
BookShelfにiterator()メソッドを追加し、利用者が走査クラスを意識せずに済むようにした
最終的に、利用者は以下のようなシンプルなコードで本棚の全ての本を順番に取り出せるようになりました。
| |
利用者は本棚の内部構造を知る必要がありません。「次があるか」と「次を取得する」という2つの操作だけで、全ての要素を処理できます。
今回学ぶこと:Iteratorパターンの全体像
実は、このシリーズで作ってきた設計には名前があります。Iteratorパターンです。
Iteratorパターンは、「GoF(Gang of Four)のデザインパターン」として知られる23のパターンの1つです。1994年に出版された『Design Patterns』という書籍で紹介され、オブジェクト指向設計の古典として世界中で読まれています。
デザインパターンとは
デザインパターンとは、ソフトウェア設計における「よくある問題」に対する「再利用可能な解決策」のことです。先人たちが試行錯誤の末にたどり着いた設計の知恵が、パターンとして整理されています。
デザインパターンを学ぶメリットは以下の通りです。
- 設計の引き出しが増え、適切な設計を素早く選択できる
- 共通の語彙を持つことで、設計について議論しやすくなる
- 先人の失敗を繰り返さずに済む
Iteratorパターンの目的
Iteratorパターンの目的は、「集合体の内部構造を公開せずに、その要素に順番にアクセスする手段を提供する」ことです。
このシリーズで直面した問題を思い出してください。
$shelf->books->@*のように内部の配列に直接アクセスするとカプセル化が崩れる- 本棚の内部構造が変更されると、利用者のコードも修正が必要になる
Iteratorパターンは、まさにこの問題を解決するためのパターンです。
Iteratorパターンの4つの要素
Iteratorパターンは、以下の4つの要素(登場人物)で構成されます。
classDiagram
class Iterator {
<<interface>>
+has_next()*
+next()*
}
class ConcreteIterator {
-aggregate
-index
+has_next()
+next()
}
class Aggregate {
<<interface>>
+iterator()*
}
class ConcreteAggregate {
-elements
+iterator()
}
Iterator <|.. ConcreteIterator : implements
Aggregate <|.. ConcreteAggregate : implements
ConcreteAggregate ..> ConcreteIterator : creates
ConcreteIterator --> ConcreteAggregate : references
| 要素 | 役割 | 本棚アプリでの実装 |
|---|---|---|
| Iterator(反復子) | 走査のインターフェースを定義する | BookIteratorRole |
| ConcreteIterator(具体的な反復子) | 実際の走査ロジックを実装する | BookShelfIterator |
| Aggregate(集合体) | イテレータを生成するインターフェースを定義する | (今回は省略) |
| ConcreteAggregate(具体的な集合体) | イテレータを返すメソッドを持つ | BookShelf |
それぞれの要素を詳しく見ていきます。
Iterator(反復子)
走査に必要なメソッドを定義するインターフェースです。本棚アプリでは、BookIteratorRoleがこの役割を担っています。
| |
has_nextとnextという2つのメソッドを要求することで、「Iteratorとは何か」を明確に定義しています。
ConcreteIterator(具体的な反復子)
Iteratorインターフェースを実装し、実際の走査ロジックを提供するクラスです。本棚アプリでは、BookShelfIteratorがこの役割を担っています。
| |
走査の具体的なロジック(インデックスを進める、現在位置の要素を返すなど)はここに実装されています。
Aggregate(集合体)
イテレータを生成するメソッドを定義するインターフェースです。本棚アプリでは明示的に作成していませんが、より厳密な実装ではAggregateRoleのようなロールを定義し、requires 'iterator'と宣言することもあります。
ConcreteAggregate(具体的な集合体)
Aggregateインターフェースを実装し、具体的なイテレータを返すクラスです。本棚アプリでは、BookShelfがこの役割を担っています。
| |
iteratorメソッドがBookShelfIteratorを返すことで、利用者は具体的な走査クラスを意識せずに済みます。
応用例:逆順イテレータの実装
Iteratorパターンの強力な点は、異なる走査方法を簡単に追加できることです。例として、本棚を逆順に巡回するReverseBookShelfIteratorを実装してみましょう。
以下は、逆順イテレータを追加した後の最終的なクラス構成です。
classDiagram
class Book {
+title
+author
}
class BookIteratorRole {
<<role>>
+has_next()*
+next()*
}
class BookShelfIterator {
-bookshelf
-index
+has_next()
+next()
}
class ReverseBookShelfIterator {
-bookshelf
-index
+has_next()
+next()
}
class BookShelf {
-books
+add_book(book)
+get_book_at(index)
+get_length()
+iterator()
+reverse_iterator()
}
BookIteratorRole <|.. BookShelfIterator : with
BookIteratorRole <|.. ReverseBookShelfIterator : with
BookShelf "1" o-- "*" Book : contains
BookShelf ..> BookShelfIterator : creates
BookShelf ..> ReverseBookShelfIterator : creates
BookShelfIterator --> BookShelf : references
ReverseBookShelfIterator --> BookShelf : references
| |
このコードのポイントは以下の通りです。
builder => 1— 初期値を動的に生成する。_build_indexメソッドが呼ばれる_build_index— 本棚の最後のインデックス(length - 1)を返すhas_next— インデックスが0以上であれば真を返すnext— 現在位置の本を取得し、インデックスを1つ戻す
ReverseBookShelfIteratorはBookIteratorRoleを適用しているため、通常のイテレータと同じインターフェースで使用できます。利用者のコードを見てみましょう。
| |
ループの書き方は全く同じです。異なるのはイテレータの生成部分だけです。これがIteratorパターンの柔軟性です。
さらに、BookShelfにreverse_iteratorメソッドを追加すれば、利用者は走査クラスの名前すら知る必要がなくなります。
完成コード
以上をまとめた完成コードを以下に示します。このコードをbookshelf.plとして保存し、実行してみましょう。
| |
実行方法
| |
実行結果
| |
シリーズのまとめ
このシリーズで学んだことを振り返ります。
- 集合体(Aggregate)は、複数のオブジェクトをまとめて管理するオブジェクトである
- カプセル化を保つためには、内部構造を外部に公開しない設計が重要である
- Iterator(反復子)は、集合体の要素を順番に取り出すための専用オブジェクトである
Moo::Roleを使ってインターフェースを定義することで、契約を明確にできる- Iteratorパターンは、走査のロジックを集合体から分離し、柔軟な設計を実現する
Iteratorパターンは、多くのプログラミング言語に組み込まれています。Perlのeach関数やPythonのfor ... in ...構文、JavaScriptのfor ... of ...構文なども、内部的にはIteratorの概念を使用しています。
次のステップへ
デザインパターンの世界には、Iteratorパターン以外にも多くのパターンがあります。興味を持った方は、以下の方向に進んでみることをお勧めします。
- GoFのデザインパターン — 『オブジェクト指向における再利用のためのデザインパターン』を読んでみる
- 他のパターンを試す — Factory、Observer、Strategyなど、身近な問題に適用できるパターンを探す
- CPANモジュールのソースコードを読む — 実際のPerlプロジェクトでパターンがどう使われているかを学ぶ
デザインパターンは「銀の弾丸」ではありません。パターンを知っているだけで良い設計ができるわけではありません。しかし、先人の知恵を学ぶことで、設計の選択肢が広がり、より良いコードを書けるようになります。
このシリーズが、皆さんのオブジェクト指向プログラミングの旅の一助になれば幸いです。
お読みいただきありがとうございました。
