Help us understand the problem. What is going on with this article?

phpで学ぶデザインパターン ~ 第1章 - Iterator ~

More than 3 years have passed since last update.

概要

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. 今本棚には1次元配列でBookを格納してますが、これを2次元配列にした途端、このままでは動かなくなるので、getBookAtを修正する必要がある。
  2. 本を追加した順ではなく、最後に追加した順に表示したい場合、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();

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away