概要
java言語で学ぶデザインパターン入門を買って学んだ内容を、PHPに書き直して整理していく予定です。
Iteratorとは
Iteratorパターンは、数え上げの抽象化
などと表現されます。
何が便利なのか、最初に普通に配列をforで回す例から、困る事を上げていって、Iteratorで解決してみます。
1. 配列をforで回す
この本では、本棚を表すBookShlef
というクラスに、本を表現するBook
クラスを追加してく例が紹介されてますので、それにそってやってきます。
<?php
class BookShelf
{
private $books = [];
private $index = 0;
/**
* 本棚に本を追加
*/
public function appendBook(Book $book)
{
$this->books[] = $book;
}
/**
* 指定した本を返す
*/
public function getBookAt($index)
{
return $this->books[$index];
}
/**
* 本棚の大きさを返す
*/
public function getLength()
{
return count($this->books);
}
}
class Book
{
private $name = '';
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}
class Main
{
public function exec()
{
$bookshelf = new BookShelf();
$bookshelf->appendBook(new Book('なぜ人は浮気を繰り返すのか'));
$bookshelf->appendBook(new Book('清楚系ビッチの生態'));
$bookshelf->appendBook(new Book('失敗する恋愛術'));
for ($i = 0; $i < $bookshelf->getLength(); $i++) {
echo $bookshelf->getBookAt($i)->getName().PHP_EOL;
}
}
}
$main = new Main();
$main->exec();
// => output
// なぜ人は浮気を繰り返すのか
// 清楚系ビッチの生態
// 失敗する恋愛術
本棚(BookShelf)にBookを3冊追加し、順に取り出してます。
まぁ良さそうですが、不便な点が2つあります。
- 今本棚には1次元配列でBookを格納してますが、これを2次元配列にした途端、このままでは動かなくなるので、
getBookAt
を修正する必要がある。 - 本を追加した順ではなく、最後に追加した順に表示したい場合、Mainクラスの実装を修正する必要がある
2. BookShelfにIteratorを実装してみる
<?php
interface OriginalIterator
{
public function hasNext();
public function next();
}
class BookShelf implements OriginalIterator
{
private $books = [];
private $index = 0;
/**
* 本棚に本を追加
*/
public function appendBook(Book $book)
{
$this->books[] = $book;
}
public function hasNext()
{
if ($this->index < $this->getLength()) {
return true;
}
return false;
}
public function next()
{
$book = $this->books[$this->index];
$this->index++;
return $book;
}
/**
* 本棚の大きさを返す
*/
public function getLength()
{
return count($this->books);
}
}
class Book
{
private $name = '';
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}
class Main
{
public function exec()
{
$bookshelf = new BookShelf();
$bookshelf->appendBook(new Book('なぜ人は浮気を繰り返すのか'));
$bookshelf->appendBook(new Book('清楚系ビッチの生態'));
$bookshelf->appendBook(new Book('失敗する恋愛術'));
while ($bookshelf->hasNext()) {
echo $bookshelf->next()->getName().PHP_EOL;
}
}
}
$main = new Main();
$main->exec();
// => output
// なぜ人は浮気を繰り返すのか
// 清楚系ビッチの生態
// 失敗する恋愛術
OriginalIteratorというインターフェイスを作り、BookShelfで実装してみました。
これで、まぁ配列の形が変わってもMainをいじる事はなく、Iteratorをいじればなんとか出来そうです。
ただ、最後に追加した順に走査したい場合、今度はその逆走用のBookShelfを作らないといけないため、この問題はまだ解決出来ないです。面倒です。
じゃあ、最後に、いよいよ本で紹介されてたIteratorパターンを紹介します。
3. 集約体から好きなイテレータを使えるようにする
試しに、追加順に表示する順走のイテレータと、最後に追加した順から表示する逆走のイテレータを2つ作って使ってみます。
<?php
interface Aggregater
{
public function orderIterator();
public function reverseIterator();
}
interface OriginalIterator
{
public function hasNext();
public function next();
}
class BookShelf implements Aggregater
{
private $books = [];
/**
* 本棚に本を追加
*/
public function appendBook(Book $book)
{
$this->books[] = $book;
}
public function getBookAt($index)
{
return $this->books[$index];
}
/**
* 本棚の大きさを返す
*/
public function getLength()
{
return count($this->books);
}
/**
* 順走イテレータ
*/
public function orderIterator()
{
return new OrderIterator($this);
}
/**
* 順走イテレータ
*/
public function reverseIterator()
{
return new ReverseIterator($this);
}
}
/**
* 順走イテレータ
*/
class OrderIterator implements OriginalIterator
{
private $aggregate;
private $index = 0;
public function __construct($aggregate)
{
$this->aggregate = $aggregate;
}
public function hasNext()
{
if ($this->index < $this->aggregate->getLength()) {
return true;
}
return false;
}
public function next()
{
$book = $this->aggregate->getBookAt($this->index);
$this->index++;
return $book;
}
}
/**
* 逆走イテレータ
*/
class ReverseIterator implements OriginalIterator
{
private $aggregate;
private $index = 0;
public function __construct($aggregate)
{
$this->aggregate = $aggregate;
$this->index = $this->aggregate->getLength() - 1;
}
public function hasNext()
{
return ($this->index >= 0);
}
public function next()
{
$book = $this->aggregate->getBookAt($this->index);
$this->index--;
return $book;
}
}
class Book
{
private $name = '';
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}
class Main
{
public function exec()
{
$bookshelf = new BookShelf();
$bookshelf->appendBook(new Book('なぜ人は浮気を繰り返すのか'));
$bookshelf->appendBook(new Book('清楚系ビッチの生態'));
$bookshelf->appendBook(new Book('失敗する恋愛術'));
$orderIterator = $bookshelf->orderIterator();
echo "順走イテレータ発動!!\n\n";
while ($orderIterator->hasNext()) {
echo $orderIterator->next()->getName().PHP_EOL;
}
$reverseIterator = $bookshelf->reverseIterator();
echo "\n\n逆走イテレータ発動!!\n\n";
while ($reverseIterator->hasNext()) {
echo $reverseIterator->next()->getName().PHP_EOL;
}
}
}
$main = new Main();
$main->exec();
// => output
// 順走イテレータ発動!!
// なぜ人は浮気を繰り返すのか
// 清楚系ビッチの生態
// 失敗する恋愛術
// 逆走イテレータ発動!!
// 失敗する恋愛術
// 清楚系ビッチの生態
// なぜ人は浮気を繰り返すのか
このように、集約体とは別でイテレータを実装し、それを使えるようにしておくことで、
呼び出し元の実装を変える事なく、集約体(Aggregater)に対する走査を自由に行うように出来ます
おまけ
@tadsan さんに教えて頂いた、PHPの組み込みクラスのIteratorを使って、foreachで回せるバージョンも作ってみました。
元のBookShelfはCountable
でオブジェクトの数を取得可能にし、ArrayAccess
でイテレータからBookShlefに配列としてアクセス出来るようにしてます。
<?php
interface Aggregater
{
public function orderIterator();
public function reverseIterator();
}
class BookShelf implements Aggregater, \ArrayAccess, \Countable
{
private $books = [];
public function count()
{
return count($this->books);
}
/**
* 本棚に本を追加
*/
public function appendBook(Book $book)
{
$this->books[] = $book;
}
public function offsetExists($index)
{
return isset($this->books[$index]);
}
public function offsetGet($index)
{
return $this->books[$index];
}
public function offsetSet($index, $book)
{
$this->books[$index] = $book;
}
public function offsetUnset($index)
{
unset($this->books[$index]);
}
/**
* 順走イテレータ
*/
public function orderIterator()
{
return new OrderIterator($this);
}
/**
* 逆走イテレータ
*/
public function reverseIterator()
{
return new ReverseIterator($this);
}
}
trait OriginalIteratable
{
private $aggregate;
private $index = 0;
public function __construct($aggregate)
{
$this->aggregate = $aggregate;
$this->rewind();
}
public function valid()
{
return ($this->index >= 0 && ($this->index < count($this->aggregate)));
}
public function current()
{
return $this->aggregate[$this->index];
}
public function key()
{
return $this->index;
}
}
class OrderIterator implements \Iterator
{
use OriginalIteratable;
private $aggregate;
private $index = 0;
public function rewind()
{
$this->index = 0;
}
public function next()
{
$book = $this->aggregate[$this->index];
$this->index++;
return $book;
}
}
class ReverseIterator implements \Iterator
{
use OriginalIteratable;
private $aggregate;
private $index = 0;
public function rewind()
{
$this->index = count($this->aggregate) - 1;
}
public function next()
{
$book = $this->aggregate[$this->index];
$this->index--;
return $book;
}
}
class Book
{
private $name = '';
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}
class Main
{
public function exec()
{
$bookshelf = new BookShelf();
$bookshelf->appendBook(new Book('なぜ人は浮気を繰り返すのか'));
$bookshelf->appendBook(new Book('清楚系ビッチの生態'));
$bookshelf->appendBook(new Book('失敗する恋愛術'));
$orderIterator = $bookshelf->orderIterator();
$reverseIterator = $bookshelf->reverseIterator();
echo "順走イテレータ発動!!\n\n";
foreach ($orderIterator as $o) {
echo $o->getName();
echo "\n\n";
}
echo "逆走イテレータ発動!!\n\n";
foreach ($reverseIterator as $o) {
echo $o->getName();
echo "\n\n";
}
}
}
$main = new Main();
$main->exec();