SeekableIterator — на примере реализации коллекции объектов

А вот ещё одна статья, написанная больше года назад, и пролежавшая всё это время в черновиках. В тот период я в качестве упражнений в TDD- и DDD- методологиях писал некий сферических фреймворк в вакууме для работы с генеалогическими деревьями, что-то в этом роде. По мере использования разных хороших ООП практик был написан ряд статей с примерами их использования в коде этого проекта. Конечно сейчас я не очень доволен тем что код не соответствует PSR, но в целом он достаточно годен для просветительских целей.


Немного лирики. Массивы в PHP — это почти наше всё. В языке доступны почти все мыслимые и не мыслимые способы для работы с ними. Благодаря этому, любое приложение на PHP использует их в самых разных целях. Но, у этого удобства есть и обратная сторона. Из за привычки хранить в массивах всё и вся (ибо удобно же!) — от конфигурации и пользовательских данных и до логики — у многих разработчиков вырабатывается рефлекс, который им мешает использовать объекты там, где это следовало бы, и толкает создать ещё один «массивчик», а манипулировать им с помощью давно и хорошо знакомых функций и конструкций. Иными словами, мне порой кажется, что историческая распространённость и доступность ассоциативных массивов в PHP, во многом тормозит раскрытие объектно-ориентированных возможностей языка у многих разработчиков, и порой негативно сказывается на дизайне многих решений.

Немного теории. На самом деле массив — это одна из реализаций абстрактного типа данных коллекция.

Теперь к делу. Использование отдельного объекта коллекции для хранения набора объектов вместо привычного массива, даёт ряд преимуществ. Например:

  • Контроль типа хранимых объектов
  • Добавление в интерфейс коллекции методов для удобства работы с хранимыми объектами
  • Реализация своего итератора для перебора данных и доступа к данным

В phamily framework для хранения детей персоны, был спроектирован специальный интерфейс:

<code>
<?php
namespace phamily\framework\models;
 
interface ChildrenCollectionInterface extends \Countable, \SeekableIterator{
 
	public function add(PersonaInterface $child);
 
	public function contains(PersonaInterface $chils);
 
}
</code>

Исходник на bitbucket.

Данный интерфейс расширяет SeekableIterator и Countable из SPL, пока парой методов:

  1. add() — для добавления объекта ребенка к коллекции.
  2. contains() — для проверки, чтобы избежать повторного добавления ребенка

Реализация SeekableIterator позволяет производить перебор с переходом к определенному элементу. Для этого метод seek() происходит установка курсора в переданное значение. Как и все итераторы, этот наследует тип Traversable, а значит класс ChildrenCollection может быть использован внутри цикла foreach().

Вот сама реализация, class ChildrenCollection:

<code>
<?php
namespace phamily\framework\models;
 
use phamily\framework\models\exceptions\LogicException;
use phamily\framework\models\exceptions\OutOfBoundsException;
 
class ChildrenCollection implements ChildrenCollectionInterface{
 
	protected $children = [];
 
	protected $parent;
 
	public function __construct(PersonaInterface $parent){
		$this->parent = $parent;
	}
 
	/*
	 * ChildrenCollectionInterface implemantation
	 */
 
	public function add(PersonaInterface $child){
		if($this->contains($child)){
			throw new LogicException("Persona already has this child");
		}
		if($this->parent === $child){
			throw new LogicException("Persona can't be parent for self");
		}
		$this->children[] = $child;
	}
 
	public function contains(PersonaInterface $child){
		return in_array($child, $this->children, true);
	}
 
	/*
	 * SPL Countable implementation
	 */
 
	public function count(){
		return count($this->children);
	}	
 
	/*
	 * SPL SeekableIterator implementation
	 */
 
	protected $position = 0;
 
	public function seek ($position) {
		if($position >= $this->count()){
			throw new OutOfBoundsException("Persona has only {$this->count()} children");
		}
		$this->position = $position;
	}
 
	public function current () {
		return $this->children[$this->position];
	}
 
	public function next () {
		++$this->position; 
	}
 
	public function key () {
		return $this->position;
	}
 
	public function valid () {
		return $this->position < $this->count();
	}
 
	public function rewind () {
		$this->position = 0;
	}
}
</code>

Исходник на bitbucket.

А вот пример как данный класс тестируется (coverage: 100%):

<code>
<?php
namespace phamily\tests\models;
 
use phamily\tests\UnitTest;
use phamily\tests\models\traits\PersonaStubTrait;
use phamily\framework\models\ChildrenCollection;
 
class ChildrenCollectionTest extends UnitTest{
 
	use PersonaStubTrait;
	const EXCEPTION_BASE_NS = '\\phamily\\framework\\models\\exceptions\\';
	public function testPutToCollection(){
		$parent = $this->createPersonaStub();
		$collection = new ChildrenCollection($parent);
 
		$childA = $this->createPersonaStub();
		$childB = $this->createPersonaStub();
 
		$collection->add($childA);
		$collection->add($childB);
 
		$this->assertEquals(2, $collection->count());
	}
 
	public function testDoubleChildAddException(){
		$parent = $this->createPersonaStub();
		$collection = new ChildrenCollection($parent);
 
		$childA = $this->createPersonaStub();
 
		$collection->add($childA);
		$this->setExpectedException(self::EXCEPTION_BASE_NS . 'LogicException');
		$collection->add($childA);
	}
 
	public function testSeekSuccess(){
		$parent = $this->createPersonaStub();
		$collection = new ChildrenCollection($parent);
 
		$childA = $this->createPersonaStub();
		$childB = $this->createPersonaStub();
		$collection->add($childA);
		$collection->add($childB);
 
		$collection->seek(1);
		$this->assertEquals(1, $collection->key());
		$this->assertEquals($childB, $collection->current());
 
		$collection->rewind();
		$this->assertEquals(0, $collection->key());
		$this->assertEquals($childA, $collection->current());
	} 
 
	public function testSeekException(){
		$parent = $this->createPersonaStub();
		$collection = new ChildrenCollection($parent);
 
		$child = $this->createPersonaStub();
		$collection->add($child);
 
		$this->setExpectedException(self::EXCEPTION_BASE_NS . 'OutOfBoundsException');
		$collection->seek(5);
	}
 
	public function testParentSelfChildException(){
		$parent = $this->createPersonaStub();
		$collection = new ChildrenCollection($parent);
 
		$this->setExpectedException(self::EXCEPTION_BASE_NS . 'LogicException');
		$collection->add($parent);
	}
}
</code>

Исходник на bitbucket.

Дискуссионный момент. К ChildrenCollectionInterface я планирую добавить ещё несколько методов, и немного изменить реализацию.

  1. Позиция ребёнка должна устанавливаться в зависимости от его возраста: старшие будут помещаться в начало коллекции.
  2. Проверки, приводящие к исключениям, стоит перенести в классы соответствующих валидаторов.
  3. Стоит подумать над методом для удаления ребёнка из коллекции.
  4. Стоит проверять на корректность тип аргумента seek() и выбрасывать InvalidArgumentException для не числовых значений.
  5. Сейчас в конструкторе принимается родитель детей. Этот объект используется только для проверки, исключающей цикличное добавление себя в качестве собственного ребёнка, но стоит подумать, не перенести ли и некоторую другую логику связанную с детодобавлением из Persona в ChildrenCollection?
Эта запись была опубликована в рубрике PHP, Статьи-заметки и отмечена метками , , , , . Добавить в закладки ссылку.

Оставить комментарий

Ваш email не будет опубликован. Обязательные поля отмечены *

Вы можете использовать это HTMLтеги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>