Итератор

Итератор (Iterator) — это поведенческий паттерн проектирования, который даёт единообразный способ обходить элементы коллекции, не раскрывая, как она устроена внутри, — массив это, хеш-таблица или дерево.
Проблема
Парк автомобилей корпоративного клиента хранится в классе Fleet в виде обычного публичного массива:
final class Fleet
{
/** @var Car[] */
public array $cars = [];
public function add(Car $car): void
{
$this->cars[] = $car;
}
}Клиентский код по всему приложению обходит парк напрямую:
foreach ($fleet->cars as $car) {
// ...
}Появляется новое требование: при заезде машины на ТО нужно находить её в парке по VIN за O(1), а не перебором всего массива. Естественное решение — сменить внутреннее хранилище на ассоциативный массив, где ключ — VIN:
public array $cars = []; // теперь ключ — VIN, значение — Car
public function add(Car $car): void
{
$this->cars[$car->vin] = $car;
}Это ломает код, который полагался на числовые индексы вроде $fleet->cars[0]. Хуже того, появляется требование планировать ТО по пробегу — обходить машины от самой изношенной к самой свежей. Раз подходящего порядка Fleet не предоставляет, сортировку приходится писать в каждом месте, где она нужна:
$sorted = $fleet->cars;
usort($sorted, fn (Car $a, Car $b) => $b->mileage <=> $a->mileage);
foreach ($sorted as $car) {
// ...
}Каждый такой участок кода знает, что $fleet->cars — это именно массив, который можно сортировать через usort(). Внутреннее устройство Fleet перестало быть деталью реализации и стало контрактом, на который завязан код по всему приложению, — и любое дальнейшее изменение структуры хранения снова требует переписывать все места, где $fleet->cars используется напрямую.
Решение
Итератор скрывает внутреннее хранилище коллекции за общим интерфейсом обхода. В PHP для этого есть встроенные интерфейсы Iterator и IteratorAggregate — Fleet реализует второй, и foreach продолжает работать как раньше, ничего не зная о том, что изменилось внутри:
final class Fleet implements IteratorAggregate
{
/** @var array<string, Car> ключ — VIN, для O(1) поиска при заезде на ТО */
private array $cars = [];
public function add(Car $car): void
{
$this->cars[$car->vin] = $car;
}
public function getIterator(): Iterator
{
return new ArrayIterator(array_values($this->cars));
}
}Хранилище стало приватным и сменилось на ассоциативный массив по VIN — но клиентский код этого не заметил:
foreach ($fleet as $car) {
// порядок добавления, внутреннее устройство Fleet скрыто
}Для обхода по пробегу заводится отдельный итератор, реализующий встроенный интерфейс Iterator вручную — current(), key(), next(), rewind(), valid():
final class MileageDescIterator implements Iterator
{
/** @var Car[] */
private array $sorted;
private int $position = 0;
/** @param array<string, Car> $cars */
public function __construct(array $cars)
{
$this->sorted = array_values($cars);
usort($this->sorted, fn (Car $a, Car $b) => $b->mileage <=> $a->mileage);
}
public function current(): Car
{
return $this->sorted[$this->position];
}
public function key(): int
{
return $this->position;
}
public function next(): void
{
$this->position++;
}
public function rewind(): void
{
$this->position = 0;
}
public function valid(): bool
{
return isset($this->sorted[$this->position]);
}
}Fleet отдаёт этот итератор отдельным методом, не трогая getIterator(), который отвечает за порядок по умолчанию:
final class Fleet implements IteratorAggregate
{
// ...
public function byMileageDesc(): Iterator
{
return new MileageDescIterator($this->cars);
}
}foreach ($fleet->byMileageDesc() as $car) {
// от самого изношенного к самому свежему — для планирования ТО
}Логика сортировки по пробегу теперь существует в одном месте вместо повторения в каждом участке кода, которому она нужна, а внутреннее хранилище Fleet можно менять и дальше, не трогая ни один из вызывающих его мест.
Из чего состоит паттерн
- Итератор (встроенный интерфейс PHP
Iterator) — общий контракт обхода: текущий элемент, ключ, переход к следующему, сброс, проверка на конец. - Конкретный итератор (
MileageDescIterator, встроенныйArrayIterator) — хранит состояние обхода (текущую позицию) отдельно от самой коллекции. - Агрегат (
IteratorAggregate, реализованFleet) — предоставляет способ получить итератор, не раскрывая, как коллекция хранит элементы. - Клиент — обходит коллекцию через
foreach, не заботясь о её внутреннем устройстве.
Итератор vs Компоновщик
Компоновщик описывает, как построить рекурсивную структуру «часть — целое», где отдельный элемент и группа элементов имеют общий интерфейс. Сам по себе он ничего не говорит о том, как эту структуру обходить, — просто определяет её форму. Итератор, наоборот, не имеет отношения к тому, как структура устроена: плоский массив, ассоциативная таблица или дерево, построенное Компоновщиком, — его задача только в том, чтобы дать единообразный способ пройтись по уже существующей структуре. Дерево, собранное Компоновщиком, обычно нуждается в собственном итераторе, который умеет рекурсивно спускаться по узлам, — эта логика обхода не имеет отношения к тому, как Компоновщик определяет саму форму дерева.
Когда применять
- Коллекция хранит элементы во внутренней структуре, которая может меняться со временем (массив → хеш-таблица → дерево), а клиентский код не должен зависеть от конкретной реализации.
- Нужно несколько независимых способов обхода одной и той же коллекции — по умолчанию, по пробегу, по дате следующего ТО — без дублирования логики сортировки в каждом месте использования.
- Обход должен поддерживать несколько одновременных независимых проходов по одной коллекции: у каждого итератора своё состояние, и два
foreachпо одной и той же коллекции не мешают друг другу.
Плюсы и минусы
Итератор скрывает внутреннее устройство коллекции от клиентского кода — оно перестаёт быть контрактом, от которого зависят другие классы, и остаётся деталью реализации, которую можно менять свободно. Новые способы обхода добавляются новыми классами итераторов, не трогая ни коллекцию, ни существующий клиентский код.
Минус — для простых плоских массивов, где обычный foreach и так прекрасно работает, отдельный класс итератора на каждый вид обхода — лишняя обёртка ради гибкости, которая может никогда не понадобиться. Каждый новый вариант обхода — это ещё один класс, и их число растёт линейно с числом нужных порядков.
Итог
Итератор нужен, когда способ хранения коллекции — деталь реализации, которая должна быть скрыта от клиента, а вариантов обхода этой коллекции больше одного. В PHP это устроено идиоматично через Iterator и IteratorAggregate: клиент продолжает писать обычный foreach, не зная, какая структура и какой порядок обхода стоят за ней на самом деле.