Итератор — это поведенческий паттерн проектирования, который даёт возможность последовательно обходить элементы составных объектов, не раскрывая их внутреннего представления.
Проблема
Коллекции — самая распространённая структура данных, которую вы можете встретить в программировании. Это набор объектов, собранный в одну кучу по каким-то критериям.
Разные типы коллекций.
Большинство коллекций выглядят как обычный список элементов. Но есть и экзотические коллекции, построенные на основе деревьев, графов и других сложных структур данных.
Но как бы ни была структурирована коллекция, пользователь должен иметь возможность последовательно обходить её элементы, чтобы проделывать с ними какие-то действия.
Но каким способом следует перемещаться по сложной структуре данных? Например, сегодня может быть достаточным обход дерева в глубину, но завтра потребуется возможность перемещаться по дереву в ширину. А на следующей неделе и того хуже — понадобится обход коллекции в случайном порядке.
Одну и ту же коллекцию можно обходить разными способами.
Добавляя всё новые алгоритмы в код коллекции, вы понемногу размываете её основную задачу, которая заключается в эффективном хранении данных. Некоторые алгоритмы могут быть и вовсе слишком «заточены» под определённое приложение и смотреться дико в общем классе коллекции.
Решение
Идея паттерна Итератор состоит в том, чтобы вынести поведение обхода коллекции из самой коллекции в отдельный класс.
Итераторы содержат код обхода коллекции. Одну коллекцию могут обходить сразу несколько итераторов.
Объект-итератор будет отслеживать состояние обхода, текущую позицию в коллекции и сколько элементов ещё осталось обойти. Одну и ту же коллекцию смогут одновременно обходить различные итераторы, а сама коллекция не будет даже знать об этом.
К тому же, если вам понадобится добавить новый способ обхода, вы сможете создать отдельный класс итератора, не изменяя существующий код коллекции.
Аналогия из жизни
Варианты прогулок по Риму.
Вы планируете полететь в Рим и обойти все достопримечательности за пару дней. Но приехав, вы можете долго петлять узкими улочками, пытаясь найти Колизей.
Если у вас ограниченный бюджет — не беда. Вы можете воспользоваться виртуальным гидом, скачанным на телефон, который позволит отфильтровать только интересные вам точки. А можете плюнуть и нанять локального гида, который хоть и обойдётся в копеечку, но знает город как свои пять пальцев, и сможет посвятить вас во все городские легенды.
Таким образом, Рим выступает коллекцией достопримечательностей, а ваш мозг, навигатор или гид — итератором по коллекции. Вы, как клиентский код, можете выбрать один из итераторов, отталкиваясь от решаемой задачи и доступных ресурсов.
Структура
Итератор описывает интерфейс для доступа и обхода элементов коллекции.
Конкретный итератор реализует алгоритм обхода какой-то конкретной коллекции. Объект итератора должен сам отслеживать текущую позицию при обходе коллекции, чтобы отдельные итераторы могли обходить одну и ту же коллекцию независимо.
Коллекция описывает интерфейс получения итератора из коллекции. Как мы уже говорили, коллекции не всегда являются списком. Это может быть и база данных, и удалённое API, и даже дерево Компоновщика. Поэтому сама коллекция может создавать итераторы, так как она знает, какие именно итераторы способны с ней работать.
Конкретная коллекция возвращает новый экземпляр определённого конкретного итератора, связав его с текущим объектом коллекции. Обратите внимание, что сигнатура метода возвращает интерфейс итератора. Это позволяет клиенту не зависеть от конкретных классов итераторов.
Клиент работает со всеми объектами через интерфейсы коллекции и итератора. Так клиентский код не зависит от конкретных классов, что позволяет применять различные итераторы, не изменяя существующий код программы.
В общем случае клиенты не создают объекты итераторов, а получают их из коллекций. Тем не менее, если клиенту требуется специальный итератор, он всегда может создать его самостоятельно.
Псевдокод
В этом примере паттерн Итератор используется для реализации обхода нестандартной коллекции, которая инкапсулирует доступ к социальному графу Facebook. Коллекция предоставляет несколько итераторов, которые могут по-разному обходить профили людей.
Пример обхода социальных профилей через итератор.
Так, итератор друзей перебирает всех друзей профиля, а итератор коллег — фильтрует друзей по принадлежности к компании профиля. Все итераторы реализуют общий интерфейс, который позволяет клиентам работать с профилями, не вникая в детали работы с социальной сетью (например, в авторизацию, отправку REST-запросов и т. д.)
Кроме того, Итератор избавляет код от привязки к конкретным классам коллекций. Это позволяет добавить поддержку другого вида коллекций (например, LinkedIn), не меняя клиентский код, который работает с итераторами и коллекциями.
// Общий интерфейс коллекций должен определить фабричный метод
// для производства итератора. Можно определить сразу несколько
// методов, чтобы дать пользователям различные варианты обхода
// одной и той же коллекции.
interface SocialNetwork is
method createFriendsIterator(profileId):ProfileIterator
method createCoworkersIterator(profileId):ProfileIterator
// Конкретная коллекция знает, объекты каких итераторов нужно
// создавать.
class Facebook implements SocialNetwork is
// ...Основной код коллекции...
// Код получения нужного итератора.
method createFriendsIterator(profileId) is
return new FacebookIterator(this, profileId, "friends")
method createCoworkersIterator(profileId) is
return new FacebookIterator(this, profileId, "coworkers")
// Общий интерфейс итераторов.
interface ProfileIterator is
method getNext():Profile
method hasMore():bool
// Конкретный итератор.
class FacebookIterator implements ProfileIterator is
// Итератору нужна ссылка на коллекцию, которую он обходит.
private field facebook: Facebook
private field profileId, type: string
// Но каждый итератор обходит коллекцию, независимо от
// остальных, поэтому он содержит информацию о текущей
// позиции обхода.
private field currentPosition
private field cache: array of Profile
constructor FacebookIterator(facebook, profileId, type) is
this.facebook = facebook
this.profileId = profileId
this.type = type
private method lazyInit() is
if (cache == null)
cache = facebook.socialGraphRequest(profileId, type)
// Итератор реализует методы базового интерфейса по-своему.
method getNext() is
if (hasMore())
result = cache[currentPosition]
currentPosition++
return result
method hasMore() is
lazyInit()
return currentPosition < cache.length
// Вот ещё полезная тактика: мы можем передавать объект
// итератора вместо коллекции в клиентские классы. При таком
// подходе клиентский код не будет иметь доступа к коллекциям, а
// значит, его не будут волновать подробности их реализаций. Ему
// будет доступен только общий интерфейс итераторов.
class SocialSpammer is
method send(iterator: ProfileIterator, message: string) is
while (iterator.hasMore())
profile = iterator.getNext()
System.sendEmail(profile.getEmail(), message)
// Класс приложение конфигурирует классы, как захочет.
class Application is
field network: SocialNetwork
field spammer: SocialSpammer
method config() is
if working with Facebook
this.network = new Facebook()
if working with LinkedIn
this.network = new LinkedIn()
this.spammer = new SocialSpammer()
method sendSpamToFriends(profile) is
iterator = network.createFriendsIterator(profile.getId())
spammer.send(iterator, "Very important message")
method sendSpamToCoworkers(profile) is
iterator = network.createCoworkersIterator(profile.getId())
spammer.send(iterator, "Very important message")
Применимость
Когда у вас есть сложная структура данных, и вы хотите скрыть от клиента детали её реализации (из-за сложности или вопросов безопасности).
Итератор предоставляет клиенту всего несколько простых методов перебора элементов коллекции. Это не только упрощает доступ к коллекции, но и защищает её данные от неосторожных или злоумышленных действий.
Когда вам нужно иметь несколько вариантов обхода одной и той же структуры данных.
Нетривиальные алгоритмы обхода структуры данных могут иметь довольно объёмный код. Этот код будет захламлять всё вокруг — будь то сам класс коллекции или часть бизнес-логики программы. Применив итератор, вы можете выделить код обхода структуры данных в собственный класс, упростив поддержку остального кода.
Когда вам хочется иметь единый интерфейс обхода различных структур данных.
Итератор позволяет вынести реализации различных вариантов обхода в подклассы. Это позволит легко взаимозаменять объекты итераторов, в зависимости от того, с какой структурой данных приходится работать.
Шаги реализации
Создайте общий интерфейс итераторов. Обязательный минимум — это операция получения следующего элемента коллекции. Но для удобства можно предусмотреть и другое. Например, методы для получения предыдущего элемента, текущей позиции, проверки окончания обхода и прочие.
Создайте интерфейс коллекции и опишите в нём метод получения итератора. Важно, чтобы сигнатура метода возвращала общий интерфейс итераторов, а не один из конкретных итераторов.
Создайте классы конкретных итераторов для тех коллекций, которые нужно обходить с помощью паттерна. Итератор должен быть привязан только к одному объекту коллекции. Обычно эта связь устанавливается через конструктор.
Реализуйте методы получения итератора в конкретных классах коллекций. Они должны создавать новый итератор того класса, который способен работать с данным типом коллекции. Коллекция должна передавать ссылку на собственный объект в конструктор итератора.
В клиентском коде и в классах коллекций не должно остаться кода обхода элементов. Клиент должен получать новый итератор из объекта коллекции каждый раз, когда ему нужно перебрать её элементы.
Преимущества и недостатки
Преимущества
Упрощает классы хранения данных.
Позволяет реализовать различные способы обхода структуры данных.
Позволяет одновременно перемещаться по структуре данных в разные стороны.
Недостатки
Не оправдан, если можно обойтись простым циклом.
Примеры реализации паттерна
Python
Сложность: 2/3
Популярность: 3/3
Применимость: Паттерн можно часто встретить в Python-коде, особенно в программах, работающих с разными типами коллекций, и где требуется обход разных сущностей.
Признаки применения паттерна: Итератор легко определить по методам навигации (например, получения следующего/предыдущего элемента и т. д.). Код использующий итератор зачастую вообще не имеет ссылок на коллекцию, с которой работает итератор. Итератор либо принимает коллекцию в параметрах конструктора при создании, либо возвращается самой коллекцией.
Этот пример показывает структуру паттерна Итератор, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
from__future__importannotationsfromcollections.abcimportIterable,IteratorfromtypingimportAny"""Для создания итератора в Python есть два абстрактных класса извстроенного модуля collections - Iterable, Iterator. Нужно реализоватьметод __iter__() в итерируемом объекте (списке), а метод __next__() витераторе."""classAlphabeticalOrderIterator(Iterator):""" Конкретные Итераторы реализуют различные алгоритмы обхода. Эти классы постоянно хранят текущее положение обхода. """""" Атрибут _position хранит текущее положение обхода. У итератора может быть множество других полей для хранения состояния итерации, особенно когда он должен работать с определённым типом коллекции. """_position:int=None""" Этот атрибут указывает направление обхода. """_reverse:bool=Falsedef__init__(self,collection:WordsCollection,reverse:bool=False)->None:self._collection=collectionself._reverse=reverseself._position=-1ifreverseelse0def__next__(self)->Any:""" Метод __next __() должен вернуть следующий элемент в последовательности. При достижении конца коллекции и в последующих вызовах должно вызываться исключение StopIteration. """try:value=self._collection[self._position]self._position+=-1ifself._reverseelse1exceptIndexError:raiseStopIteration()returnvalueclassWordsCollection(Iterable):""" Конкретные Коллекции предоставляют один или несколько методов для получения новых экземпляров итератора, совместимых с классом коллекции. """def__init__(self,collection:list[Any]|None=None)->None:self._collection=collectionor[]def__getitem__(self,index:int)->Any:returnself._collection[index]def__iter__(self)->AlphabeticalOrderIterator:""" Метод __iter__() возвращает объект итератора, по умолчанию мы возвращаем итератор с сортировкой по возрастанию. """returnAlphabeticalOrderIterator(self)defget_reverse_iterator(self)->AlphabeticalOrderIterator:returnAlphabeticalOrderIterator(self,True)defadd_item(self,item:Any)->None:self._collection.append(item)if__name__=="__main__":# Клиентский код может знать или не знать о Конкретном Итераторе# или классах Коллекций, в зависимости от уровня косвенности,# который вы хотите сохранить в своей программе.collection=WordsCollection()collection.add_item("First")collection.add_item("Second")collection.add_item("Third")print("Straight traversal:")print("\n".join(collection))print("")print("Reverse traversal:")print("\n".join(collection.get_reverse_iterator()),end="")
Straighttraversal:
First
Second
Third
Reversetraversal:
Third
Second
First
PHP
Сложность: 2/3
Популярность: 2/3
Применимость: Паттерн можно часто встретить в PHP-коде, особенно в программах, работающих с разными типами коллекций, и где требуется обход разных сущностей.
PHP имеет встроенный интерфейс для поддержки итераторов (Iterator), на основании которого можно строить свои Итераторы, совместимые с остальным PHP-кодом.
Признаки применения паттерна: Итератор легко определить по методам навигации (например, получения следующего/предыдущего элемента и т. д.). Код использующий итератор зачастую вообще не имеет ссылок на коллекцию, с которой работает итератор. Итератор либо принимает коллекцию в параметрах конструктора при создании, либо возвращается самой коллекцией.
Этот пример показывает структуру паттерна Итератор, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
<?php/*** Конкретные Итераторы реализуют различные алгоритмы обхода. Эти классы* постоянно хранят текущее положение обхода.*/classAlphabeticalOrderIteratorimplements\Iterator{/** * @var WordsCollection */private$collection;/** * @var int Хранит текущее положение обхода. У итератора может быть * множество других полей для хранения состояния итерации, особенно когда он * должен работать с определённым типом коллекции. */private$position=0;/** * @var bool Эта переменная указывает направление обхода. */private$reverse=false;publicfunction__construct($collection,$reverse=false){$this->collection=$collection;$this->reverse=$reverse;}publicfunctionrewind(){$this->position=$this->reverse?count($this->collection->getItems())-1:0;}publicfunctioncurrent(){return$this->collection->getItems()[$this->position];}publicfunctionkey(){return$this->position;}publicfunctionnext(){$this->position=$this->position+($this->reverse?-1:1);}publicfunctionvalid(){returnisset($this->collection->getItems()[$this->position]);}}/*** Конкретные Коллекции предоставляют один или несколько методов для получения* новых экземпляров итератора, совместимых с классом коллекции.*/classWordsCollectionimplements\IteratorAggregate{private$items=[];publicfunctiongetItems(){return$this->items;}publicfunctionaddItem($item){$this->items[]=$item;}publicfunctiongetIterator():Iterator{returnnewAlphabeticalOrderIterator($this);}publicfunctiongetReverseIterator():Iterator{returnnewAlphabeticalOrderIterator($this,true);}}/*** Клиентский код может знать или не знать о Конкретном Итераторе или классах* Коллекций, в зависимости от уровня косвенности, который вы хотите сохранить в* своей программе.*/$collection=newWordsCollection();$collection->addItem("First");$collection->addItem("Second");$collection->addItem("Third");echo"Straight traversal:\n";foreach($collection->getIterator()as$item){echo$item."\n";}echo"\n";echo"Reverse traversal:\n";foreach($collection->getReverseIterator()as$item){echo$item."\n";}
Straighttraversal:
First
Second
Third
Reversetraversal:
Third
Second
First
Так как PHP уже имеет встроенный интерфейс Итератора, который предоставляет удобную интеграцию с циклами foreach, очень легко создать собственные итераторы для обхода практически любой мыслимой структуры данных.
Этот пример паттерна Итератор предоставляет лёгкий доступ к CSV-файлам.
<?php/*** Итератор CSV-файлов.** @author Josh Lockhart*/classCsvIteratorimplements\Iterator{constROW_SIZE=4096;/** * Указатель на CSV-файл. * * @var resource */protected$filePointer=null;/** * Текущий элемент, который возвращается на каждой итерации. * * @var array */protected$currentElement=null;/** * Счётчик строк. * * @var int */protected$rowCounter=null;/** * Разделитель для CSV-файла. * * @var string */protected$delimiter=null;/** * Конструктор пытается открыть CSV-файл. Он выдаёт исключение при ошибке. * * @param string $file CSV-файл. * @param string $delimiter Разделитель. * * @throws \Exception */publicfunction__construct($file,$delimiter=','){try{$this->filePointer=fopen($file,'rb');$this->delimiter=$delimiter;}catch(\Exception$e){thrownew\Exception('The file "'.$file.'" cannot be read.');}}/** * Этот метод сбрасывает указатель файла. */publicfunctionrewind():void{$this->rowCounter=0;rewind($this->filePointer);}/** * Этот метод возвращает текущую CSV-строку в виде двумерного массива. * * @return array Текущая CSV-строка в виде двумерного массива. */publicfunctioncurrent():array{$this->currentElement=fgetcsv($this->filePointer,self::ROW_SIZE,$this->delimiter);$this->rowCounter++;return$this->currentElement;}/** * Этот метод возвращает номер текущей строки. * * @return int Номер текущей строки. */publicfunctionkey():int{return$this->rowCounter;}/** * Этот метод проверяет, достигнут ли конец файла. * * @return bool Возвращает true при достижении EOF, в ином случае false. */publicfunctionnext():bool{if(is_resource($this->filePointer)){return!feof($this->filePointer);}returnfalse;}/** * Этот метод проверяет, является ли следующая строка допустимой. * * @return bool Если следующая строка является допустимой. */publicfunctionvalid():bool{if(!$this->next()){if(is_resource($this->filePointer)){fclose($this->filePointer);}returnfalse;}returntrue;}}/*** Клиентский код.*/$csv=newCsvIterator(__DIR__.'/cats.csv');foreach($csvas$key=>$row){print_r($row);}
Реализуйте паттерн Итератор для бинарного дерева поиска. Создайте три итератора: InOrderIterator (симметричный: лево → корень → право), PreOrderIterator (прямой: корень → лево → право) и PostOrderIterator (обратный: лево → право → корень). Клиентский код должен обходить дерево через единый интерфейс, не зная о структуре узлов.
Задача 2. Итератор плоского списка из вложенной коллекции
Реализуйте паттерн Итератор для коллекции NestedList, которая содержит смесь целых чисел и вложенных списков произвольной глубины. Итератор FlatIterator должен «разворачивать» структуру и выдавать все числа последовательно, как если бы список был одноуровневым. Клиентский код не должен знать о вложенности.
Реализуйте декоративные итераторы: FilterIterator (пропускает элементы, не удовлетворяющие предикату) и MapIterator (применяет функцию к каждому элементу). Оба принимают любой итерируемый объект и могут быть скомбинированы в цепочку. Покажите их работу на коллекции пользователей.
<?phpclassUser{publicfunction__construct(publicint$id,publicstring$name,publicint$age,publicbool$active,publicfloat$score,){}}// TODO: реализовать:// FilterIterator implements Iterator// — __construct(iterable $source, callable $predicate)// — пропускает элементы, для которых $predicate($item) === false//// MapIterator implements Iterator// — __construct(iterable $source, callable $transform)// — применяет $transform к каждому элементу$users=[newUser(1,'Анна',28,true,4.8),newUser(2,'Иван',17,true,3.2),newUser(3,'Мария',32,false,4.5),newUser(4,'Пётр',25,true,4.9),newUser(5,'Света',16,true,3.8),newUser(6,'Алекс',45,true,4.7),];// Ожидаемый клиентский код:// $activeAdults = new FilterIterator($users, fn($u) => $u->active && $u->age >= 18);// $highRated = new FilterIterator($activeAdults, fn($u) => $u->score >= 4.5);// $names = new MapIterator($highRated, fn($u) => "{$u->name} ({$u->score})");// echo "Топ пользователи:\n";// foreach ($names as $name) echo " $name\n";// // Анна (4.8)// // Пётр (4.9)// // Алекс (4.7)
Задача 4. Итератор страниц (пагинация)
Реализуйте паттерн Итератор для постраничного обхода данных. Класс PaginatedIterator получает функцию fetch_page(page, size) и автоматически подгружает следующую страницу по мере обхода. Итератор должен корректно завершаться, когда страницы закончились, и поддерживать стандартный цикл for.
Задача 5. Итератор графа (обход в ширину и глубину)
Реализуйте паттерн Итератор для обхода графа. Класс Graph хранит вершины и рёбра. Создайте два итератора: BFSIterator (обход в ширину, использует очередь) и DFSIterator (обход в глубину, использует стек). Оба принимают стартовую вершину и корректно обрабатывают циклы (посещают каждую вершину не более одного раза).
// echo "BFS от A: ";
// foreach (new BFSIterator($g, 'A') as \(v) echo "\)v ";
// // A B C D E F
// echo "\nDFS от A: ";
// foreach (new DFSIterator($g, 'A') as \(v) echo "\)v ";
// // A C F B E D (порядок зависит от реализации)