はじめに
@nqounetです。
シリーズ「本棚アプリで覚える集合体の巡回」の第3回です。
前回の振り返り
前回は、本棚のすべての本を一覧表示する機能を実装しました。その過程で、以下の問題点が明らかになりました。
$shelf->books->@*のように内部の配列に直接アクセスするとカプセル化が崩れるget_lengthとget_book_atを使う方法でも、「長さがあってインデックスでアクセスできる」という実装詳細に依存している
本棚の内部構造が変更されると、それを利用するすべてのコードを修正する必要があるという問題が残っていました。
今回学ぶこと:Iterator(反復子)ロール
今回は、この問題を解決するために「Iterator(反復子)」という概念を導入します。
Iteratorとは、集合体の要素を順番に取り出すための専用オブジェクトです。利用者は「次の要素があるか」と「次の要素を取得する」という2つの操作だけを知っていれば、集合体の内部構造を一切知る必要がありません。
この概念を実装するために、前シリーズ「Mooで覚えるオブジェクト指向プログラミング」で学んだMoo::Roleを活用します。ロールを使うことで、Iteratorが持つべきインターフェース(メソッドの契約)を明確に定義できます。
BookIteratorRoleを定義する
今回作成するクラス構成を以下に示します。
classDiagram
class BookIteratorRole {
<<role>>
+has_next()*
+next()*
}
class BookShelfIterator {
-bookshelf
-index
+has_next()
+next()
}
class BookShelf {
-books
+add_book(book)
+get_book_at(index)
+get_length()
}
BookIteratorRole <|.. BookShelfIterator : with
BookShelfIterator --> BookShelf : references
まず、Iteratorが持つべきインターフェースをロールとして定義します。
| |
このコードのポイントは以下の通りです。
use Moo::Role— ロール(インターフェース)を定義するためのモジュールrequires 'has_next'— このロールを適用するクラスはhas_nextメソッドを実装しなければならないrequires 'next'— このロールを適用するクラスはnextメソッドを実装しなければならない
requiresは「このメソッドを実装することを要求する」という宣言です。ロール自体はメソッドの実装を持たず、「このメソッドが必要である」という契約だけを定義しています。
2つのメソッドの役割
has_next— 次の要素があれば真、なければ偽を返すnext— 次の要素を返し、内部の位置を進める
この2つのメソッドがあれば、利用者は以下のようなシンプルなループで全要素を処理できます。
| |
本棚が配列で実装されているか、ハッシュで実装されているか、データベースから取得しているか——利用者はそれを知る必要がありません。
BookShelfIteratorクラスを実装する
次に、BookIteratorRoleを適用した具体的なイテレータクラスを作成します。
| |
このコードのポイントは以下の通りです。
with 'BookIteratorRole'— ロールを適用する。has_nextとnextの実装が必須になるbookshelf— 走査対象の本棚への参照(読み取り専用)index— 現在の走査位置(読み書き可能、初期値0)has_next— 現在位置が本棚の長さより小さければ真を返すnext— 現在位置の本を取得し、位置を1つ進めて返す
責任の分離
ここで重要なのは、走査のロジックがBookShelfIteratorに集約されたという点です。
BookShelfクラス — 本を管理する責任を持つBookShelfIteratorクラス — 本棚を走査する責任を持つ
それぞれのクラスが単一の責任を持つことで、コードが整理され、変更にも強くなります。本棚の内部構造が変更されても、影響を受けるのはBookShelfIteratorだけです。利用者のコードは一切変更する必要がありません。
完成コード
以上をまとめた完成コードを以下に示します。このコードをbookshelf.plとして保存し、実行してみましょう。
| |
実行方法
| |
実行結果
| |
まとめ
- Iterator(反復子)は、集合体の要素を順番に取り出すための専用オブジェクトである
Moo::Roleを使ってhas_nextとnextを要求するインターフェースを定義したBookShelfIteratorクラスは走査のロジックを担当し、BookShelfから責任を分離した- イテレータを使うと、利用者は集合体の内部構造を知らなくても全要素を処理できる
次回予告
今回、イテレータを作成しましたが、利用者が直接BookShelfIterator->new(bookshelf => $shelf)と書く必要があります。これでは、利用者がイテレータクラスの名前を知っている必要があり、まだ本棚の実装詳細への依存が残っています。
次回は、本棚自身がイテレータを生成する仕組みを導入します。利用者は$shelf->iteratorと書くだけで、適切なイテレータを取得できるようになります。
お楽しみに。
