Примеры шаблонов проектирования или как написать свой PHP framework. Часть 4: Итератор

В этой короткой статье я опишу реализацию шаблона проектирования итератор (Iterator). Как я и обещал, мы рассмотри класс ArrayList, упомянутый в прошлой статье. Давайте сначала рассмотрим некоторые особенности класса ArrayList, а потом я расскажу о реализации паттерна итератор.

Зачем нужен класс ArrayList? Многие умные дядьки скажут, что он и нафиг не нужен, и, и без этого можно обойтись. Конечно можно, но некоторым, таким как я, это вполне может показаться удобным на столько, что данный функционал можно просто использовать. Но не будем тратить лишние символы, которые и так на данный момент не сжимаются ни каким gzip-ом или чем-то типа того. Первое и, я считаю, главное, что делает данный класс - это нормальный обход генерации ошибки уровня E_NOTICE, при доступе к неинициализированному элементу массива. То есть, когда мы делаем так:


Listing №1 (PHP)
<?php
error_reporting(E_ALL);
$ar = array();
echo $ar[0]
?>


мы получаем ошибку вида:

результат
Notice: Undefined offset: 0 in <имя файла>

Чтобы этого не происходило, мы можем либо подавить возможную ошибку знаком @ либо проверить индекс массива. Или вообще не обращать внимания на ошибки уровня E_NOTICE. Но так как последнее считается плохой практикой, как, по моему мнению, также и собака, то остается лишь проверять индекс массива с помощью isset(). Я предлагаю спрятать это в функционале нашего ArrayList. Фактически ArrayList будет тем же самым обычным php-массивом, но с встроенной проверкой на присутствие индекса. Также вместо доступа к массиву через квадратные скобки, как то [идетификатор ключа], я предлагаю осуществлять доступ через магические методы __get() и __set(). То есть работа с массивом будет осуществляться через оболочку объекта. Следующий пример показывает эквивалентные обращения к стандартному массиву и к объектному массиву ArrayList:


Listing №2 (PHP)
//инициализация ссылки
$NativeArray = array();
$ObjectArray = new ArrayList();
//запись элемента
$NativeArray['Name'] = 'Vova';
$ObjectArray->Name = 'Vova';
//чтение элемента
if(isset($NativeArray['Name'])) {
  echo $NativeArray['Name'];
}
echo $ObjectArray->Name;


Думаю, некоторые из вас могут посчитать обращение к ArrayList более удобным или вполне приемлемым, чем к обычному массиву. Если вы думаете не так, то можете дальше не читать. Если же вы согласны с тем, что в некоторых ситуациях это может пригодиться, то вы вполне адекватно воспримите реализацию выше описанного функционала:


Listing №3 (PHP)
class ArrayList {
  /**
   * @var array
   */
  private $_Items;
 
  public function __construct()
  {
    $this->_Items = array();
  }
  /**
   * @param string $VarName
   * @return mixed
   */
  public function __get($VarName)
  {
    if(isset($this->_Items[$VarName])) {
      return $this->_Items[$VarName];
    }
    else {
      return NULL;
    }
  }
  /**
   * @param string $VarName
   * @param mixed $VarValue
   */
  public function __set($VarName, $VarValue)
  {
    $this->_Items[$VarName] = $VarValue;
  }


Теперь, если запрошенный элемент не присутствует в нашем ArrayList, то будет просто возвращен NULL без всяких генераций E_NOTICE. При этом не надо писать скобки и кавычки. Конечно массивы в php это очень мощный механизм и, я думаю, глупо бы было даже думать о реализации в нашем объекте их полного функционала. Поэтому, для класса ArrayList я предпочту сделать поддержку еще пока только одной операции - циклический проход по элементам ArrayList.

В этом месте мы плавно перешли к шаблону проектирования итератор (Iterator), суть которого и заключается в предоставлении последовательного доступа к каким-то данным сущности. Классически различают итератор и агрегатор итератора. Агрегатор и является той сущностью, обход элементов которой предоставляется итератором. То есть, попросту говоря, классически шаблон проектирования итератор формируется двумя объектами: объектом-агрегатором, данные которого необходимо циклически обойти, и самим объектом-итератором, который позволяет выполнить такой обход. В данном случае обычно объект-агрегатор предоставляет управляющей конструкции (циклу) доступ к своему объекту-итератору. Как же это реализовать в PHP?

Нам, можно сказать, повезло, что PHP 5 изначально скомпилирован с поддержкой интерфейса итератора (Iterator) и агрегатора (IteratorAggregate). Вам остается лишь указать, что объект реализует данный интерфейс и реализовать непосредственно его методы. Причем для обхода элементов в цикле foreach достаточно реализовать любой из интерфейсов. Если вы реализуете интерфейс агрегатора, то вам нужно будет реализовать один метод: getIterator(), который должен возвратить объект, реализующий интерфейс Iterator. Но зачем это делать, если объект может сразу реализовать интерфейс Iterator? Дело в том, что классы обычно служат для представления данных и методов взаимодействия с этими данными. Поэтому реализация интерфейса Iterator может просто не вписываться в основную функциональность класса. Кроме этого, во время итераций по объекту сведения об итерации (текущая позиция) сохраняются в самом объекте, что иногда делает невозможным организацию вложенных итераций. По этим причинам лучше возложить функциональность интерфейса Iterator на отдельный класс, а в основном классе реализовать интерфейс IteratorAggregate. Так как эти две причины практически не относятся к классу ArrayList, то мы просто реализуем в нем интерфейс Iterator.

Интерфейс итератора в PHP описывается так:


Listing №4 (PHP)
Iterator extends Traversable {
/* Methods */
  abstract public mixed current ( void )
  abstract public scalar key ( void )
  abstract public void next ( void )
  abstract public void rewind ( void )
  abstract public boolean valid ( void )
}


Таким образом, наш ArrayList должен реализовать пять методов:


Listing №5 (PHP)
class ArrayList implements Iterator {
  /**
   * @see Iterator
   */
  public function rewind()
  {
    reset($this->_Items);
  }
  /**
   * @see Iterator
   */
  public function current()
  {
    return current($this->_Items);
  }
  /**
   * @see Iterator
   */
  public function key()
  {
    return key($this->_Items);
  }
  /**
   * @see Iterator
   */
  public function next()
  {
    return next($this->_Items);
  }
  /**
   * @see Iterator
   */
  public function valid()
  {
    return ($this->current() !== FALSE);
  }


Тогда мы можем так же, как и обычный массив, передать его в цикл foreach:


Listing №6 (PHP)
$Errors = new ArrayList();
$Errors->LoginError = 'Login is empty';
$Errors->PasswordError = 'Password is small';
foreach($Errors as $key=>$value) {
  echo "$key: \"$value\"<br>";
}


Тут можно подумать, почему просто нельзя реализовать интерфейс IteratorAggregate и вернуть массив Items:


Listing №7 (PHP)
class ArrayList implements IteratorAggregate {
 
  public function getIterator()
  {
    return $this->_Items;
  }


Этот код не будет работать, потому что обычный массив не является объектом, реализующим интерфейс Iterator, а конструкция foreach, при передачи ей объекта интерфейса IteratorAggregate, ожидает от его функции getIterator() именно объект, реализующий интерфейс Iterator. Если вы хотите сделать таким образом, то вам необходимо будет преобразовать массив в объект-итератор. Для этого вам поможет встроенный класс ArrayObject, который превращает массив в объект-итератор (и не только):


Listing №8 (PHP)
class ArrayList implements IteratorAggregate {
 
  public function getIterator()
  {
    return new ArrayIterator($this->_Items);
  }


Или же вам придется самим вызвать getIterator() или какой-то метод, возвращающий массив Items в коде foreach. Я, пожалуй, остановлюсь на варианте с реализацией Iterator. Помимо этого, давайте реализуем еще встроенный интерфейс Countable, который позволяет функции count подсчитывать количество элементов в объекте-реализаторе:


Listing №9 (PHP)
class ArrayList implements Iterator, Countable {
 
  /**
   * @see Countable
   */
  public function Count()
  {
    return count($this->_Items);
  }


Теперь мы можем спокойно вычислить количество элементов ArrayList:


Listing №10 (PHP)
$Errors = new ArrayList();
$Errors->LoginError = 'Login is empty';
$Errors->PasswordError = 'Password is small';
foreach($Errors as $key=>$value) {
  echo "$key: \"$value\"<br>";
}
echo 'Total errors: ' . count($Errors);


На этом данную тему итераторов прикрываю. Продолжение следует...


зы: а вообще всё это может делать встроенный ArrayObject
Теги:
Iterator, паттерн, framework, ArrayObject, IteratorAggregate, Countable, итератор, агрегатор
Добавлено: 08 Августа 2018 07:56:54 Добавил: Андрей Ковальчук Нравится 0
Добавить
Комментарии:
Нету комментариев для вывода...