こんにちは、エンジニアの王です。
今回は、Iteratorパターンをご紹介したいと思います。
「iterate」は「◯◯を繰り返す」という意味で、プログラミングにおいてはもっぱら「for」や「while」などを用いた「反復処理」をするという意味で使われます。
「Iterator パターン」は、集約オブジェクト(コレクションオブジェクト)の中の要素を列挙する手段を提供して、具体的な列挙方法を集約オブジェクトから隠蔽することで、列挙方法を抽象化します。
なんだか難しいように思えますが、心配することはありません。後ほど実際にコードを見れば、すぐに分かると思います。
Iterator パターン
集約オブジェクトって?
何らかの「情報の集まり」を格納したオブジェクトのことです。「配列」が最も単純な集約オブジェクトでしょう。
// 「名前」集約オブジェクト
$name_list = [ 'Matumoto Jun', 'Kobayashi Kentaro', 'Mudata Shuichi', 'Murakami Ryu', 'Kamijou Touma' ];
集約オブジェクトをiterateする
ただの配列ですので、foreachで回すだけです。
// 「名前」集約オブジェクト
foreach( $name_list as $name ){
echo $name;
}
列挙の手続きが煩雑になってきたら・・・
上記ではごくごく単純な配列の列挙ですから、foreachなどで回して何らかの処理をする分には、特に問題は感じなかったでしょう。しかし、現場ではこんな単純なケースは少ないと思います。
複雑なケースでは、Iteratorパターンを検討しましょう。
まずは例で「書籍」クラスを考えてみましょう。
例:「書籍」クラス
先ほどの$name_listを書籍クラスの中の「執筆者一覧」だと仮定しましょう。後日集約オブジェクトの$name_listの中身が変わって、下記になったとします。
$name_list = [
[
'family-name' => 'Matumoto',
'given-name' => 'Jun',
],
[
'family-name' => 'Kobayashi',
'given-name' => 'Kentaro',
],
[
'family-name' => 'Mudata',
'given-name' => 'Shuichi',
],
[
'family-name' => 'Kamijou',
'given-name' => 'Touma',
],
[
'family-name' => 'Murakami',
'given-name' => 'Ryu',
],
];
データの構造が変わったので、同じ出力をするためには以前に書いたコードを書き直しますよね。
foreach ( $name_list as $name_obj ) {
foreach ( $name_obj as $family_name => $given_name ) {
echo "$family_name $given_name";
}
}
月日が流れ、今度はidが追加されて、なおかつ昇順で出力する仕様になって・・・
$name_list = [
[
'family-name' => 'Matumoto',
'given-name' => 'Jun',
'id' => 5
],
[
'family-name' => 'Kobayashi',
'given-name' => 'Kentaro',
'id' => 1,
],
[
'family-name' => 'Mudata',
'given-name' => 'Shuichi',
'id' => 2
],
[
'family-name' => 'Kamijou',
'given-name' => 'Touma',
'id' => 4
],
[
'family-name' => 'Murakami',
'given-name' => 'Ryu',
'id' => 3
],
];
さあ、またまた改修作業が必要です。
これが一箇所で使われているのならまだしも、10箇所、20箇所で使っていたら全て置き換えるのは大変です。
そうなったら、たいていの人はこの処理を関数化・メソッド化しようと考えだすでしょう。そういうときこそ「Iteratorパターン」!
問題
集約オブジェクトの個々の要素のデータ構造が変わったり、ソート方法が変わったりなどといったとき、以前に書いたコードを変える必要が出てきてしまいます(OCPを違反する)。
また、異なる集約オブジェクトを処理する際にクラス内で判定処理を挟んで、「このタイプであればこう、あのタイプであればこう」といった具合にタイプ別で列挙処理をすることで、クラスの柔軟性が損なわれてしまいます。
解決策
集約オブジェクトに「iteratorオブジェクト」を提供させることであっさり解決します。
「iteratorオブジェクト」は集約オブジェクトを列挙する方法を知っているので、クライアント(iteratorを使うオブジェクト)は実装の詳細を知る必要はなく、「iteratorオブジェクト」の使い方さえ知っていればいいです。
そうすれば、後からいくら集約オブジェクトのタイプが増えても、クライアント側の記述そのままで大丈夫になりますので、プログラムの柔軟性がぐっと上がります。
実装コード
<?php
/*=================================================================
インターフェイス
=================================================================*/
// Aggregate Interface
interface AuthorList {
/**
* Iteratorを返すメソッド
* @return AuthorIterator
*/
public function createIterator();
}
// Iterator Interface
interface AuthorIterator {
/**
* 次の要素があるかどうかを判定する
* @return bool
*/
public function hasNext();
/**
* 次の要素を返し、ポインターを進める
* @return mixed
*/
public function next();
}
/*=================================================================
Iteratorの実装
=================================================================*/
// Iteratorクラス、このクラス内で列挙の手順を記述する。
// AuthorIteratorインターフェイスを実装するようにする。
class AuthorList_Simple_Iterator implements AuthorIterator {
private $authors;
private $position = 0;
function __construct( $authors ) {
$this->authors = $authors;
}
public function hasNext() {
return isset( $this->authors[ $this->position ] );
}
public function next() {
return $this->authors[ $this->position ++ ];
}
}
// 別のIteratorクラス、列挙の手順が一個前と違うけど、
// 同じAuthorIteratorインターフェイスを持っているので、使う側からしたら差異はない
class AuthorList_Detailed_Iterator implements AuthorIterator {
protected $authors;
private $position = 0;
function __construct( $authors ) {
$this->authors = $authors;
}
public function hasNext() {
return isset( $this->authors[ $this->position ] );
}
public function next() {
$author = $this->authors[ $this->position ++ ];
$family_name = $author['family-name'];
$given_name = $author['given-name'];
return $family_name . ' - ' . $given_name;
}
}
// 更に別のIteratorクラス、「AuthorList_Detailed_Iterator」と似ているので、継承する
class AuthorList_Detailed_OrderByID_Iterator extends AuthorList_Detailed_Iterator implements AuthorIterator {
function __construct( $authors ) {
usort( $authors, function ( $a, $b ) {
return (int) $a['id'] > (int) $b['id'];
} );
$this->authors = $authors;
}
}
/*=================================================================
Aggregate(集約オブジェクト)の実装
=================================================================*/
// クラス間で共有しているメソッドたちを分離しておく。
trait AuthorList_Methods {
private $author_list;
function __construct( $authors ) {
$this->author_list = $authors;
}
public function add_to_list( $author ) {
$this->author_list[] = $author;
}
public function get_author_list() {
return $this->author_list;
}
}
// 集約オブジェクト 著者一覧[簡易版]
class Authors_Simple implements AuthorList {
use AuthorList_Methods;
public function createIterator() {
return new AuthorList_Simple_Iterator( $this->author_list );
}
}
// 集約オブジェクト 著者一覧[詳細版]
class Authors_Detailed implements AuthorList {
use AuthorList_Methods;
public function createIterator() {
return new AuthorList_Detailed_Iterator( $this->author_list );
}
}
// 集約オブジェクト 著者一覧[詳細+並び替え版]
class Authors_Detailed_OrderByID implements AuthorList {
use AuthorList_Methods;
public function createIterator() {
return new AuthorList_Detailed_OrderByID_Iterator( $this->author_list );
}
}
/*=================================================================
クライアントである「書籍クラス」
=================================================================*/
class Book {
/** @type AuthorIterator $author_iterator */
private $author_iterator;
function __construct( AuthorList $author_list ) {
// Iteratorオブジェクトを持たせる
$this->author_iterator = $author_list->createIterator();
}
function print_authors() {
// Iteratorオブジェクトを使う
while ( $this->author_iterator->hasNext() ) {
$author = $this->author_iterator->next();
echo $author . '<br>';
}
}
}
/*=================================================================
メイン処理
=================================================================*/
// 著者一覧のデータ[詳細]
$authors_detailed_array = [
[
'family-name' => 'Matumoto',
'given-name' => 'Jun',
'id' => 5
],
[
'family-name' => 'Kobayashi',
'given-name' => 'Kentaro',
'id' => 1,
],
[
'family-name' => 'Mudata',
'given-name' => 'Shuichi',
'id' => 2
],
[
'family-name' => 'Kamijou',
'given-name' => 'Touma',
'id' => 4
],
[
'family-name' => 'Murakami',
'given-name' => 'Ryu',
'id' => 3
],
];
// 著者一覧のデータ[簡易]
$authors_simple_array = [ 'Matumoto Jun', 'Kobayashi Kentaro', 'Mudata Shuichi', 'Murakami Ryu', 'Kamijou Touma' ];
// それぞれ違う集約オブジェクトで書籍クラスをインスタンス化
$book_a = new Book( new Authors_Detailed( $authors_detailed_array ) );
$book_b = new Book( new Authors_Simple( $authors_simple_array ) );
$book_c = new Book( new Authors_Detailed_OrderByID( $authors_detailed_array ) );
?>
<section>
<h1>book_a</h1>
<?php $book_a->print_authors(); ?>
</section>
<section>
<h1>book_b</h1>
<?php $book_b->print_authors(); ?>
</section>
<section>
<h1>book_c</h1>
<?php $book_c->print_authors(); ?>
</section>
出力:
book_a
Matumoto – Jun
Kobayashi – Kentaro
Mudata – Shuichi
Kamijou – Touma
Murakami – Ryubook_b
Matumoto Jun
Kobayashi Kentaro
Mudata Shuichi
Murakami Ryu
Kamijou Toumabook_c
Kobayashi – Kentaro
Mudata – Shuichi
Murakami – Ryu
Kamijou – Touma
Matumoto – Jun
クラス図
今回の場合、AuthorListがAggregate、AuthorIteratorがIteratorインターフェイスに当たりますよね。AggregateがIteratorを返して、Iteratorの内部で要素を列挙するといった構造になっています。
まとめ
今回は「Iterator パターン」を使って、コレクション内の要素を列挙する仕事をIteratorに任せっきりにして、列挙する具体的な手順を抽象化することで、プログラムの拡張性や柔軟性が向上したことがコードを読んでおわかりいただけたかと思います。
ちなみに、PHPにはもともとIterator インターフェイスがあって、このインターフェイスを実装することで、コレクションをforeachで回せるようになります。Iteratorの概念そのものは同じです。本記事で言及している古典的なIteratorに比べて実装する必要があるメソッドは“5個”と、ちょいと多い感じです。
詳しくは公式の説明ページに譲るとして、また次回!
【エンジニア王による、PHPで学ぶデザインパターン講座】
※ 【PHPで学ぶデザインパターン入門】第1回 Strategyパターン
※ 【PHPで学ぶデザインパターン入門】第2回 Decoratorパターン
※ 【PHPで学ぶデザインパターン入門】第3回 Stateパターン
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。