19.06 2013

Iterator (Итератор)

Перед прочтением ознакомьтесь с введением в паттерны проектирования на PHP, в котором описаны принятые соглашения и понятия. Данная статья дополняется с некоторой периодичностью, так что если вы ее читали ранее, не факт что данные не изменились.

Итератор (Iterator) относится к классу поведенческих паттернов. Используется в составных объектах. Предоставляет доступ к своим внутренним полям не раскрывая их структуру.

Зачастую этот паттерн используется вместо массива объектов, чтобы не только предоставить доступ к элементам, но и наделить некоторой логикой. Это может быть ограничение доступа, сортировка или любая другая операция над множеством объектов.

На этом применение паттерна не ограничивается, иногда его удобно использовать для создания своеобразного бесконечного конвейера. Правда с приходом PHP 5.5 на это место придут генераторы

Структура

PHP поддерживает итераторы из коробки, для этого есть 2 интерфейса.

Iterator (Итератор)

Как видите, структура паттерна очень простая. Iterator это общий интерфейс, позволяющий реализовать произвольную логику итераций. IteratorAggregate более общий, позволяющий использовать готовые итераторы.

Немного о методах интерфейса Iterator

  • Метод current() возвращает текущий элемент
  • Метод next() перемещает указатель на следующий элемент
  • Метод key() возвращает индекс текущего элемента
  • Метод valid() проверяет, существует ли текущий элемент или нет
  • Метод rewind() переводит указатель текущего элемента на первый

Таким образом, реализуя все методы итератора можно будет делать так:

foreach($iterator as $key => $value) {
    // do something
}

что соответствует вызовам метода итератора

rewind
valid // return true
current
key
// do something
next
valid // return true
current
key
// do something
...
next
valid // return false

и на php можно было бы написать как-то так

$iterator->rewind();
while($iterator->valid()) {
    $value = $iterator->current();
    $key = $iterator->key();
    // do something
    $iterator->next();
}

Реализуем простой итератор по массиву

class MyArrayIterator implements Iterator
{
    protected $array = array();

    public function __construct(array $array)
    {
        $this->array = $array;
    }

    public function current()
    {
        return current($this->array);
    }

    public function next()
    {
        next($this->array);
    }

    public function key()
    {
        return key($this->array);
    }

    public function valid()
    {
        return isset($this->array[$this->key()]);
    }

    public function rewind()
    {
        reset($this->array);
    }
}

class MyIteratorAggregate implements IteratorAggregate
{
    public function getIterator()
    {
        return new ArrayIterator([1, 2, 3, 4]);
    }
}

// Example
$iterator = new MyArrayIterator([1, 2, 3, 5]);

// output array
var_dump(iterator_to_array($iterator));

// output all values of array
foreach ($iterator as $value) {
    var_dump($value);
}

// aggregate
$iteratorAggregate = new MyIteratorAggregate();
foreach($iteratorAggregate as $value) {
    var_dump($value);
}

полный исходник

Встроенные итераторы

В PHP есть набор уже реализованных итераторов список довольно длинный

Среди них есть и ArrayIterator, который я использовал в примере выше

К практике

Реализуем корзину покупок

class Purchase
{
    protected $cost;

    public function __construct($cost)
    {
        $this->cost = $cost;
    }

    public function getCost()
    {
        return $this->cost;
    }
}

class Cart implements IteratorAggregate
{
    protected $purchases;

    public function addPurchase(Purchase $purchase)
    {
        $this->purchases[] = $purchase;
    }

    public function getCost()
    {
        $cost = 0;
        foreach ($this->purchases as $purchase) {
            $cost += $purchase->getCost();
        }
        return $cost;
    }

    public function getIterator()
    {
        // Я использовал интерфейс IteratorAggregate, так как его гораздо
        // проще реализовывать.
        // Если мне понадобится добавить дополнительную логику,
        // я выполню рефакторинг и реализую интерфейс Iterator
        return new ArrayIterator($this->purchases);
    }
}

$cart = new Cart();
$cart->addPurchase(new Purchase(10));
$cart->addPurchase(new Purchase(15));

var_dump($cart->getCost()); // 25

foreach($cart as $purchase) {
    var_dump($purchase->getCost());
}

полный исходник

Вполне резонный вопрос будет, зачем здесь использовать итератор? Можно было заменить getIterator на getPurchases и немного подредактировать код. Да, это так. Но не всегда можно отделаться так легко.

Давайте рассмотрим другой пример. Предположим у нас есть новостной блок, на который выводится 10 новостей.

Скажем он был реализован как-то так:

/** @var Post[] $news */
$news = $this->getService()->getLatestNews();

// где-то
foreach($news as $post) {
    echo $post->getTitle();
    // something else
}

Предположим что метод getLatestNews используется во многих частях системы, и все клиенты этого кода ожидают в результате массив объектов. При этом оба куска код находятся на достаточном отдалении друг от друга, что бы взаимодействовать им было бы довольно затруднительно. И единственным связующим звеном является переменная $news

Наша задача вывести не только блок новостей, но и общее количество новостей в архиве.

Красивым решением было бы заменить ответ getLatestNews() на объект в который инкапсулированы новости и количество.

/** @var Posts $news */ // !!!
$news = $this->getService()->getLatestNews();

// где-то
foreach($news->getPosts() as $post) { // !!!
    echo $post->getTitle();
    // something else
}

echo $news->getArchiveCount(); // !!!

Но исходя из условия задачи, для этого придется переписать большое количество кода.

Здесь на помощь может прийти итератор.

class Post
{

    protected $title;

    public function setTitle($title)
    {
        $this->title = $title;
    }

    public function getTitle()
    {
        return $this->title;
    }

}

class PostContainer implements IteratorAggregate
{

    protected $posts;

    protected $archiveCount;

    public function __construct(array $posts, $archiveCount)
    {
        $this->posts = $posts;
        $this->archiveCount = $archiveCount;
    }

    public function getArchiveCount()
    {
        return $this->archiveCount;
    }

    public function getIterator()
    {
        return new ArrayIterator($this->posts);
    }

}

// Перепишем getLatestNews, что бы она возвращала не массив новостей, а контейнер новостей
return new PostContainer(
    $this->_getLatestNews(),
    $this->getArchiveCount()
);

// Тогда остальной код не изменится

/** @var PostContainer $news */
$news = $this->getService()->getLatestNews();

// где-то
foreach ($news as $post) {
    echo $post->getTitle();
    // something else
}

// А здесь мы можем получить, то что хотели
echo $news->getArchiveCount();

полный исходник

В этом случае удалось обойтись малой кровью.

Применяйте этот паттерн, если

  • необходимо один и тот же объект обходить разными способами
  • необходимо в один и тот же момент иметь несколько состояний обхода объекта
comments powered by Disqus