Итератор (Iterator)
Даёт обойти коллекцию по одному элементу, не раскрывая, как она устроена внутри.
Проблема
Каталог магазина — дерево категорий: «Каталог» → «Электроника» → «Телефоны» и так далее. Меню нужно показать его в глубину (раздел, затем его подразделы), поиску — по уровням (сначала верхние категории, потом вложенные). Если каждый клиент обходит дерево сам, он знает, что у категории есть поле children, держит свой стек или очередь и повторяет один и тот же цикл в пяти местах. Стоит перенести дерево в плоскую таблицу с parent_id, и правки расползаются по всему коду.
Клиенту на самом деле нужно одно: получить категории по очереди и, может быть, остановиться на нужной.
Решение
Коллекция CategoryTree сама отдаёт обходы. Обход по умолчанию — в глубину: дерево можно сразу передать в foreach / for ... of / range. Обход в ширину — отдельный метод breadthFirst, который возвращает итератор BreadthFirstIterator. Клиент видит только «что-то, по чему можно пройти циклом», а стек, очередь и поле children остаются внутри.
Положение обхода хранит итератор, а не коллекция. Поэтому два обхода одного дерева идут независимо, и коллекции не нужны поля вроде currentIndex.
Структура
Коллекция — это неизменяемая Category и обёртка CategoryTree, которая наружу отдаёт только обходы. Обход в глубину написан генератором: цикл со стеком, где каждый yield отдаёт категорию и замораживает функцию до следующего запроса. Дети кладутся в стек в обратном порядке, чтобы первый ребёнок вышел первым. Генератор — тот же итератор, только его состояние (стек и место остановки) за нас хранит язык.
Как это собирается
Дерево собирает слой загрузки каталога: из таблицы категорий приходят плоские строки "id, родитель, имя", и функция сборки рекурсивно складывает из них Category, а корень заворачивает в CategoryTree. Строка с несуществующим родителем (Toys ссылается на id 9) в дерево не попадает: загрузчик собирает такие строки в список "orphans", чтобы показать их в админке, а не терять молча.
Дальше каждая страница берёт свой обход и ничего не знает об устройстве дерева. Карта сайта проходит всё дерево обычным циклом в глубину. Верхнее меню берёт breadthFirst, пропускает корень и останавливается после трёх пунктов: обход ленивый, поэтому большое дерево до конца не проходится.
Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.
Реализации
Итератор обхода в ширину написан руками, как в классическом описании паттерна: объект с очередью необработанных категорий и методом «дай следующую». Его полезно сравнить с генератором из коллекции: разница между обходами — только стек против очереди, а разница между способами записи — кто хранит состояние. В Go отдельного объекта нет: итератор — функция iter.Seq, а «ручной» шаг даёт iter.Pull в тестах.
| Язык | Особенность |
|---|---|
| PHP | IteratorAggregate с генератором для foreach; обход в ширину — класс с пятью методами Iterator и SplQueue |
| Go | iter.Seq[Category] и range-over-func (Go 1.23+); обход останавливается, когда yield вернул false; iter.Pull превращает его в пошаговый |
| Python | __iter__ возвращает генератор; обход в ширину — класс с __iter__/__next__ и deque |
| TypeScript | [Symbol.iterator] и генератор *depthFirst; обход в ширину — класс с next(), возвращающий { done, value } |
Запуск: make test T=patterns/behavioral/iterator
Плюсы и минусы
Тесты прогоняют одну клиентскую функцию names по обоим обходам: она принимает что угодно итерируемое и получает разный порядок — в глубину Catalog, Electronics, Phones, Laptops, Home, Kitchen, в ширину Catalog, Electronics, Home, Phones, Laptops, Kitchen. Третий тест берёт два итератора одного дерева, сдвигает первый и проверяет, что второй остался в начале. Последний тест фиксирует особенность языка: в PHP ручной итератор можно перемотать (rewind) и пройти ещё раз, в Python и TS он одноразовый, а в Go вместо этого проверяется, что break останавливает обход посередине.
- ✅ Клиент не знает устройства коллекции: дерево можно хранить иначе, клиенты не изменятся.
- ✅ Разные порядки обхода — разные итераторы, коллекция не растёт условиями.
- ✅ Несколько обходов одной коллекции идут одновременно и не мешают друг другу.
- ✅ Обход ленивый: клиент может остановиться, и остаток не вычисляется.
- ❌ Для массива в памяти это лишний слой: обычный цикл по индексам проще.
- ❌ Ручной итератор — заметно больше кода, чем генератор, и легко ошибиться в его состоянии.
- ❌ Если коллекцию меняют во время обхода, итератор видит её непредсказуемо: правила для этого придётся придумать отдельно.
Когда не применять
- Коллекция — обычный массив или список: язык уже даёт итератор, своя обёртка ничего не добавит.
- Клиенту нужен произвольный доступ (
items[42]) или длина без обхода: итератор даёт только «следующий». - Над элементами нужно сделать много разных операций, зависящих от типа узла: это скорее Посетитель (Visitor), а обход остаётся итератору.
Отличия между языками
- PHP —
foreachпонимает два контракта:Iterator(пять методов, состояние в объекте) иIteratorAggregate(вернуть итератор). Генератор реализуетIteratorсам, но его нельзя перемотать после начала обхода, поэтому коллекция отдаёт каждый раз новый генератор, а не хранит один. - Go — до 1.23 итераторы писали как структуры с
Next()(bufio.Scanner,sql.Rows). Теперь стандарт — функцияiter.Seq, по которой работаетrange; объекта-итератора нет вовсе. Еслиyieldвернулfalse, функция обязана выйти: продолжение послеbreak— паника во время выполнения. - Python — протокол из двух методов:
__iter__и__next__, конец — исключениеStopIteration. Генератор пишет оба за вас, поэтому ручной класс встречается редко. Итератор одноразовый: второй проход по тому же объекту пуст, а по коллекции — полный, потому что__iter__создаёт новый. - TypeScript —
for ... ofработает с любым объектом, у которого есть[Symbol.iterator]. Генератор (function*) возвращает объект, который одновременно итератор и итерируемое, как и ручной класс с[Symbol.iterator]() { return this; }.
Практика
Учебное дерево целиком лежит в памяти, и ленивость обхода там ничего не экономит. В жизни итератор чаще всего прячет то, что дорого достать: страницы внешнего API, строки из базы, строки файла. Практика: постраничный обход API заказов показывает итератор, который запрашивает следующую страницу, только когда клиент до неё дошёл, не грузит лишнего при раннем выходе и честно сообщает об ошибке на середине.
Связанные темы
- Практика: постраничный обход API заказов — ленивая подгрузка страниц, ранний выход и ошибка на середине
- Компоновщик (Composite) — дерево, которое удобно обходить итератором
- Стратегия (Strategy) — порядок обхода тоже можно выбирать как взаимозаменяемый алгоритм