Итератор (Iterator) — шаблон проектирования

31 января 2020
Шаблон предлагает схему для обхода списка элементов. Идея паттерна заключается в том, чтобы за реализацию обхода элементов отвечал отдельный класс-итератор, а не клиентский код или методы класса элементов (контейнера элементов). Это даёт возможность в случае изменения класса элементов сохранить клиентский код и сделать общую реализацию гибкой. 

ПРИМЕР ЗАДАЧИ: обойти очередь событий и вызвать обработчики для определённого вида событий.

Код реализации паттерна на языке PHP. Скачать исходники (ZIP, 4 Кб)

В PHP есть готовые классы, реализующие итераторы https://www.php.net/manual/ru/spl.iterators.php, а также есть языковая конструкция foreach для обхода элементов массива и публичных свойства объекта. В рамках данной статьи, встроенные в PHP итераторы и конструкция foreach использоваться не будут. 

event.class.php

Содержит класс Event, представляющее событие. Состоит из следующих свойств и методов:
  • $name – название события. Название определяет тип события. Например, название PageShow соответствует событию загрузки страницы, а MailSended соответствует событию отправки письма;
  • $handler – обработчик события. В момент срабатывания события вызывается данный обработчик;
  • $nextEvent – ссылка на следующий объект события. Ссылка используется для связывания событий в цепочку;
  • Конструктор __construct($name, $handler) – в момент создания объекта задаёт имя события и обработчик;
  • getName() – возвращает название события;
  • handle() – запускает выполнение обработчика события;
  • setNextEvent($nextEvent) – устанавливает ссылку на следующий объект события. Используется для связывания событий в цепочку.
<?

/**
 * Событие
 */
class Event
{
    /**
     * Название события
     * @var string
     */
    private $name = '';

    /**
     * Обработчик события
     * @var callable
     */
    private $handler = null;

    /**
     * Ссылка на следующее событие. Используется для связи событий в очередь
     * @var Event
     */
    public $nextEvent = null;


    /**
     * Конструктор. Создаёт объект события
     * @param string $name - название события
     * @param string $handler - обработчик события (функция, вызываемая при срабатывании события)
     */
    public function __construct($name, $handler)
    {
        $this->name = $name;
        $this->handler = $handler;
    }

    /**
     * Возвращает название события
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Выполнить обработчик
     */
    public function handle()
    {
        if (is_callable($this->handler)) {
            call_user_func($this->handler);
        } else {
            throw new Exception('Ошибка. У события «'.$this->name.'» не задан обработчик (функция обратного вызова)');
        }
    }

    /**
     * Установка ссылки на следующее событие. Используется для связывания событий в цепочку
     * @param Event $nextEvent
     */
    public function setNextEvent($nextEvent)
    {
        $this->nextEvent = $nextEvent;
    }
}

?>

observer.class.php

Представляет класс наблюдателя событий Observer. Содержит следующие свойства и методы:
  • $firstEvent – хранит ссылку на первое событие в очереди (цепочке) событий;
  • $lastEvent – хранит ссылку на последнее событие в очереди событий;
  • addEvent($name, $handler) – добавляет событие в очередь наблюдателя. В аргументе $name указывается название события, а в $handler задаётся его обработчик;
  • event($name) – запускает срабатывание обработчиков события, название которого задано в аргументе $name. Метод реализует свой собственный обход очереди событий;
  • eventByIterator($name) – аналогично, вызывает срабатывание обработчиков события, название которого задано в аргументе $name. Метод реализует обход очереди с помощью класса-итератора IteratorObserverEvents;
<?
//Подключаем класс для работы с событиями
require_once(__DIR__.'/event.class.php');

/**
 * Наблюдатель событий (служит для учёта и работы с событиями)
 */
class Observer
{
    /**
     * Ссылка на первое событие в очереди
     * @var Event
     */
    public $firstEvent = null;

    /**
     * Ссылка на последнее событие в очереди
     * @var Event
     */
    public $lastEvent = null;

    /**
     * Добавляет событие в наблюдателя
     * @param string $name - имя события
     * @param callable $handler - обработчик события
     */
    public function addEvent($name, $handler)
    {
        $event = new Event($name, $handler);

        //Установка ссылки на первый элемент списка
        if (is_null($this->firstEvent)) {
            $this->firstEvent = $event;
        }

        //Если в очереди есть события, то у прошлого последнего события ставим в указатель $nextEvent ссылку на новое последнее событие
        if (is_null($this->lastEvent) == false) {
            $this->lastEvent->setNextEvent($event);
        }

        //Обновляем ссылку на последнее событие
        $this->lastEvent = $event;
    }

    /**
     * Срабатывание определённых событий, находящихся в очереди
     * @param string $name - имя события, которое нужно вызвать
     */
    public function event($name)
    {
        $currentEvent = $this->firstEvent;
        while (is_object($currentEvent) == true) {
            //Если имя текущего события совпадает с запрашиваемым, то запускаем его обработчик
            if ($currentEvent->getName() == $name) {
                $currentEvent->handle();
            }

            $currentEvent = $currentEvent->nextEvent;
        }
    }

    /**
     * Срабатывание определённых событий, находящихся в очереди
     * @param string $name - имя события, которое нужно вызвать
     */
    public function eventByIterator($name)
    {
        //Подключаем классы для работы с итераторами
        require_once(__DIR__.'/iterators.classes.php');

        $iteratorEvents = new IteratorObserverEvents($this);
        while($currentEvent = $iteratorEvents->getNext()) {
            //Если имя текущего события совпадает с запрашиваемым, то запускаем его обработчик
            if ($currentEvent->getName() == $name) {
                $currentEvent->handle();
            }
        }
    }
}

?>

iterators.classes.php

Содержит абстрактный класс итератора AbstractIterator и конкретный итератор в виде класса IteratorObserverEvents.

Абстрактный класс AbstractIterator служит для объявления интерфейса итераторов. Содержит следующие свойства и методы:
  • $firstItem – содержит ссылку на первый элемент в цепочке элементов;
  • $currentItem – содержит ссылку на текущий элемент из процесса обхода элементов;
  • currentItem() – возвращает текущий элемент;
  • reset() – сбрасывает указатель текущего элемента, устанавливая его на первый элемент очереди элементов;
  • next() – абстрактный метод. Переводит указатель текущего элемента на следующий элемент. Метод вернёт true, если указатель удалось перевести и false в противном случае;
  • isEnd() – абстрактный метод. Проверяет, достигнут ли конец цепочки элементов. Метод вернёт true, если достигнут конец цепочки элементов и false в противном случае.

Класс  IteratorObserverEvents унаследован от абстрактного класса AbstractIterator и представляет конкретную реализацию итератора для обхода событий, находящихся под управлением наблюлателя Observer. Помимо родительских свойств и методов, класс дополнительно раскрывает следующие свойства и методы:
  • $observer – содержит ссылку на объект наблюдателя событий Observer;
  • Конструктор __construct($observer) –  устанавливает ссылку на объект наблюдателя событий Observer и ставит указатель первого обходимого элемента (события) на первый элемент очереди событий из объекта $observer;
  • reset() – сбрасывает указатель текущего элемента, устанавливая его в значение null. Также обновляет указатель первого элемента цепочки, ставя его на первый элемент цепочки из объекта наблюдателя событий, находящегося в свойстве $observer;
  • next() – переводит указатель текущего элемента на следующий элемент. Метод вернёт true, если указатель удалось перевести и false в противном случае. Здесь, в отличие от абстрактного класса, метод получил конкретную реализацию;
  • isEnd() – проверяет, достигнут ли конец цепочки элементов. Метод вернёт true, если достигнут конец цепочки элементов и false в противном случае. Метод также получил конкретную реализацию;
  • getNext() – передвигает указатель текущего элемента на следующий элемент (внутри вызывается метод next()) и возвращает его. Метод вернёт элемент или false, если следующие элементы (события) отсутствуют, либо цепочка пустая.
<?

/**
 * Абстрактный класс итератора
 */
abstract class AbstractIterator
{
    /**
     * Ссылка на первый элемент в цепочке элементов
     * @var mixed
     */
    protected $firstItem = null;

    /**
     * Ссылка на текущий элемент из процесса обхода элементов
     * @var mixed
     */
    protected $currentItem = null;

    /**
     * Возвращает текущий элемент
     * @return mixed
     */
    public function currentItem()
    {
        return $this->currentItem;
    }

    /**
     * Сбрасывает указатель текущего элемента, устанавливая его на первый элемент очереди элементов
     */
    public function reset()
    {
        $this->currentItem = $this->firstItem;
    }

    /**
     * Переводит указатель текущего элемента на следующий элемент
     * @return bool - вернёт 'true', если указатель удалось перевести и 'false' в противном случае
     */
    abstract public function next();

    /**
     * Проверяет, достигнут ли конец цепочки элементов
     * @return bool - вернёт 'true', если достигнут конец цепочки элементов и 'false' в противном случае
     */
    abstract public function isEnd();
}

/**
 * Итератор для обхода событий, находящихся под управлением класса Observer
 */
class IteratorObserverEvents extends AbstractIterator
{
    /**
     * Содержит ссылку на объект класса Observer
     * @var Observer
     */
    private $observer = null;

    /**
     * Конструктор. Устанавливает ссылку на объект наблюдателя событий Observer и ставит
     * ссылку на первый элемент очереди событий.
     * @param Observer $observer
     */
    public function __construct($observer)
    {
        $this->observer = $observer;
        $this->firstItem = $this->observer->firstEvent;
    }

    /**
     * Сбрасывает указатель текущего элемента, устанавливая его в значение null.
     * Также обновляет указатель первого элемента цепочки, ставя его на первый 
     * элемент из объекта наблюдателя событий Observer
     */
    public function reset()
    {
        $this->firstItem = $this->observer->firstEvent;
        $this->currentItem = null;
    }

    /**
     * Передвигает указатель текущего элемента на следующий элемент в очереди событи
     * @return bool - вернёт 'true' в случае успешного передвижения и 'false' в противном случае
     */
    public function next()
    {
        //Если не достигнут конец очереди, то передвигает указатель текущего элемента на следующий элемент
        if ($this->isEnd() == false) {
            //Если текущий элемент является объектом, то используем у него ссылку на следующий элемент
            if (is_object($this->currentItem) == true) {
                $this->currentItem = $this->currentItem->nextEvent;
                return true;
            } 
            /*
            Если текущий элемент НЕ является объектом, то предполагается, что мы находимся в начале цепочки. 
            Тогда сбрасываем указатели и указатель текущего элемента передвигает на первый элемент цепочки элементов
            */
            else {
                $this->reset();

                if (is_object($this->firstItem) == false) {
                    return false;
                } else {
                    $this->currentItem = $this->firstItem;
                    return true;
                }
            }
        } else {
            return false;
        }
    }

    /**
     * Проверяет, достигнут ли конец цепочки элементов
     * @return bool - вернёт 'true', если достигнут конец цепочки элементов и 'false' в противном случае
     */
    public function isEnd()
    {
        if (is_object($this->currentItem) == true && is_object($this->currentItem->nextEvent) == false) {
            return true;
        } elseif (is_object($this->firstItem) == false && is_object($this->currentItem) == false) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Передвигает указатель текущего элемента на следующий элемент и возвращает его
     * @return mixed - вернёт элемент цепочки элементов или 'false', если следующих элементов нету или цепочка пустая
     */
    public function getNext()
    {
        if ($this->isEnd() == false) {
            $this->next();
            return $this->currentItem;
        } else {
            return false;
        }
    }
}

?>

test.php

Содержит тестовый скрипт, в котором создаётся объект наблюдателя событий $observer и затем в него добавляются пять событий – три события PageShow и два события MailSended. В обработчике каждого события мы выводим на экран определённое сообщение.

Затем происходит срабатывание события PageShow с помощью метода:
$observer->event('PageShow');

Тем самым, мы заставляем выполнить обработчики этих событий. Обход событий осуществляется напрямую в методе event($name) класса Observer.

Далее снова вызываем событие PageShow, но уже с помощью метода:
$observer->eventByIterator('PageShow');

Данный метод eventByIterator($name) для обхода событий использует итератор IteratorObserverEvents. С его помощью клиентский код обхода сводится к созданию объекта итератора и получению элементов события методом $iteratorEvents->getNext()
$iteratorEvents = new IteratorObserverEvents($this);
while($currentEvent = $iteratorEvents->getNext()) {
    //Если имя текущего события совпадает с запрашиваемым, то запускаем его обработчик
    if ($currentEvent->getName() == $name) {
        $currentEvent->handle();
    }
}

Таким образом, всю логику обхода событий наблюдателя Observer мы спрятали в класс-итератор IteratorObserverEvents

Результат работы скрипта test.php
Вариант обхода событий без отдельного класса-итератора
-----------------------
Показ страницы (событие 1)
Показ страницы (событие 2)
Показ страницы (событие 3)


Вариант обхода событий с отдельным классом-итератором
-----------------------
Показ страницы (событие 1)
Показ страницы (событие 2)
Показ страницы (событие 3)

Помимо архитектурной гибкости, шаблон «Итератор» позволяет для одной и той же очереди, одного и того же массива применять разные способы обхода, реализуя для каждого из способов свой собственный  итератор.