一回デザインパターンについてちゃんと本を読んでおこうかなーと思ったので。本書のサンプルコードはJavaですが、私はJavaはある程度読めるけど、書く方はからきしなので、サンプルコードはPHPで書きます。本書の練習問題にはJavaの言語仕様に依存するものも多いですが、デザインパターン自体はオブジェクト指向言語に共通して使える概念なので、PHPでもほとんど問題ありません。
なお、本記事に掲載したサンプルコードは https://github.com/ryo-utsunomiya/design_pattern からダウンロードできます。
Iteratorパターンとは
繰り返しを表現するためのパターンです。機能的にはループ(for, while等)と変わりませんが、統一的なインターフェースを提供している点が大きな違いです。ループでは、オブジェクトを使う側がオブジェクトの状態を知っている必要があるのに対し、Iteratorでは、決まったAPIを呼び出しさえすれば、期待する結果が得られます。
サンプルコードは、「本」を収める「本棚」と、「本棚」を走査するための「本棚Iterator」を実装しています。
まずはIteratorのインタフェースを作成します。これを実装したクラスであれば、どんなクラスであれ同じ操作を行えます。
<?php
namespace my;
interface Iterator
{
/**
* @return bool
*/
public function hasNext();
/**
* @return mixed
*/
public function next();
}
次に、オブジェクトの集合であり、Iteratorを返す役割を持つインタフェースを定義します。
<?php
namespace my;
interface Aggregate
{
/**
* @return Iterator
*/
public function iterator();
}
インタフェースが定義できたので、具体的なオブジェクトを実装していきます。まずは、「本」。
<?php
namespace my;
class Book
{
/**
* @var string
*/
private $name;
/**
* @param string $name
*/
public function __construct($name)
{
$this->name = (string)$name;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
}
次に、「本」の集合である「本棚」。「本棚」は、Aggregateインタフェースを実装します。
<?php
namespace my;
class BookShelf implements Aggregate
{
/**
* @var Book[]
*/
private $books = [];
/**
* @var int
*/
private $last = 0;
/**
* @param $index
* @return Book|null
*/
public function getBookAt($index)
{
if (isset($this->books[$index])) {
return $this->books[$index];
} else {
return null;
}
}
/**
* @param Book $book
*/
public function appendBook(Book $book)
{
$this->books[$this->last] = $book;
$this->last++;
}
/**
* @return int
*/
public function getLength()
{
return $this->last;
}
/**
* @return BookShelfIterator
*/
public function iterator()
{
return new BookShelfIterator($this);
}
}
最後に、本棚を走査する本棚Iterator。
<?php
namespace my;
class BookShelfIterator implements Iterator
{
/**
* @var BookShelf
*/
private $bookShelf;
/**
* @var int
*/
private $index = 0;
/**
* @param BookShelf $bookShelf
*/
public function __construct(BookShelf $bookShelf)
{
$this->bookShelf = $bookShelf;
}
/**
* @return bool
*/
public function hasNext()
{
return $this->index < $this->bookShelf->getLength();
}
/**
* @return Book
*/
public function next()
{
$book = $this->bookShelf->getBookAt($this->index);
$this->index++;
return $book;
}
}
これで、クラス定義はできました。以下のようなコードで実行ができます。
<?php
namespace my;
require_once __DIR__ . '/../autoload.php';
$bookShelf = new BookShelf();
$bookShelf->appendBook(new Book('Around the World in 80 Days'));
$bookShelf->appendBook(new Book('Bible'));
$bookShelf->appendBook(new Book('Cinderella'));
$bookShelf->appendBook(new Book('Daddy-Long-Legs'));
$it = $bookShelf->iterator();
while ($it->hasNext()) {
$book = $it->next();
echo $book->getName(), PHP_EOL;
}
では、実行してみます。まず、以下のような形でファイルを配置します。
├── Iterator
│ ├── iterator.php
│ └── my
│ ├── Aggregate.php
│ ├── Book.php
│ ├── BookShelf.php
│ ├── BookShelfIterator.php
│ └── Iterator.php
└── autoload.php
autoload.phpは以下の内容です。PSR-0のオートローダーです。
<?php
/**
* @see http://www.php-fig.org/psr/psr-0/
* @param string $className
*/
function autoload($className)
{
$className = ltrim($className, '\');
$fileName = '';
$namespace = '';
if ($lastNsPos = strrpos($className, '\')) {
$namespace = substr($className, 0, $lastNsPos);
$className = substr($className, $lastNsPos + 1);
$fileName = str_replace('\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
}
$fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';
require $fileName;
}
spl_autoload_register('autoload');
iterator.phpは以下。
<?php
namespace my;
require_once __DIR__ . '/../autoload.php';
$bookShelf = new BookShelf();
$bookShelf->appendBook(new Book('Around the World in 80 Days'));
$bookShelf->appendBook(new Book('Bible'));
$bookShelf->appendBook(new Book('Cinderella'));
$bookShelf->appendBook(new Book('Daddy-Long-Legs'));
$it = $bookShelf->iterator();
while ($it->hasNext()) {
$book = $it->next();
echo $book->getName(), PHP_EOL;
}
これを実行すると、本の名前が4回表示されます。
PHPにおけるIterator
ここでは、Iteratorパターンを理解するため、独自のIteratorインタフェースを実装しました。しかし、PHPには組み込みのIteratorやIteratorのインタフェースが用意されています。PHPの場合、独自にインタフェースやIteratorを実装する必要はそれほどないと思います。
以下は、IteratorAggregateを使った例です。$booksは配列なので、PHP組み込みのArrayIteratorを使っています。
<?php
namespace my;
class PhpBookShelf implements IteratorAggregate
{
/**
* @var Book[]
*/
private $books = [];
/**
* @var int
*/
private $last = 0;
/**
* @param $index
* @return Book|null
*/
public function getBookAt($index)
{
if (isset($this->books[$index])) {
return $this->books[$index];
} else {
return null;
}
}
/**
* @param Book $book
*/
public function appendBook(Book $book)
{
$this->books[$this->last] = $book;
$this->last++;
}
/**
* @return int
*/
public function getLength()
{
return $this->last;
}
/**
* @return ArrayIterator
*/
public function getIterator()
{
return new ArrayIterator($this->books);
}
}
ArrayIteratorを使わず、自前でIteratorを実装するなら以下のようになるでしょう。
<?php
namespace my;
class PhpBookShelfIterator implements Iterator
{
/**
* @var BookShelf
*/
private $bookShelf;
/**
* @var int
*/
private $index = 0;
/**
* @param BookShelf $bookShelf
*/
public function __construct(BookShelf $bookShelf)
{
$this->bookShelf = $bookShelf;
}
/**
* Return the current element
*
* @return mixed Can return any type.
*/
public function current()
{
return $this->bookShelf->getBookAt($this->index);
}
/**
* Move forward to next element
*
* @return void Any returned value is ignored.
*/
public function next()
{
$this->index++;
}
/**
* Return the key of the current element
*
* @return mixed scalar on success, or null on failure.
*/
public function key()
{
return $this->index;
}
/**
* Checks if current position is valid
*
* @return boolean The return value will be casted to boolean and then evaluated.
* Returns true on success or false on failure.
*/
public function valid()
{
return !is_null($this->bookShelf->getBookAt($this->index));
}
/**
* Rewind the Iterator to the first element
*
* @return void Any returned value is ignored.
*/
public function rewind()
{
$this->index = 0;
}
}