Также известен как Iterator

Итератор

Суть паттерна

Даёт возможность последовательно обходить все элементы составного объекта, не раскрывая его внутреннего представления.

Проблема

Коллекции — самая частая структура данных, которую вы можете встретить в программировании. Это набор объектов, собранный в одну кучу по каким-то причинам.

Коллекция может быть списком, массивом, деревом и так далее. Но как бы она ни была структурирована, пользователь должен иметь возможность последовательно обходить элементы коллекции чтобы проделывать с ними какие-то действия.

Способ обхода тоже может быть разным. Сегодня вам достаточно простого обхода элементов по порядку. Но завтра может понадобиться обход по отсортированному списку элементов, а послезавтра — фильтрация или случайный обход элементов.

Решение

Идея паттерна Итератор в том, чтобы вынести поведение обхода коллекции из самой коллекции в отдельный класс.

Объект итератор будет отслеживать состояние обхода, текущую позицию в коллекции и сколько элементов ещё осталось обойти. Одну и ту же коллекцию смогут одновременно обходить различные итераторы, а сама коллекция не будет даже знать об этом. А если понадобится добавить новый способ обхода, вы создадите новый класс итератора, не изменяя код коллекции.

Аналогия из жизни

Туристический гид

Вы планируете полететь в Рим и обойти все достопримечательности за пару дней. Приехав, вы можете долго петлять узкими улочками, пытаясь найти Колизей.

А можете плюнуть и нанять локального гида, который хоть и обойдётся в копеечку, но знает город как свои пять пальцев и сможет посвятить вас во все городские легенды.

Не беда, если у вас ограниченный бюджет — вы можете воспользоваться виртуальным гидом, скачанным на телефон, который позволит отфильтровать только интересные вам точки.

Таким образом, Рим выступает коллекцией достопримечательностей; Гид, навигатор или путеводитель — вашим итератором по коллекции. Вы, как клиентский код, можете выбрать один из них, опираясь на решаемую задачу и доступные ресурсы.

Структура

Схема структуры классов паттерна Итератор
  1. Коллекция задаёт интерфейс получения итератора из коллекции.

    Обратите внимание, реальная коллекция необязательно должна быть списком. Это может быть как многомерный массив, так и база данных или дерево Компоновщика.

  2. Конкретная коллекция реализует метод получения итератора, подходящего к данному типу коллекции.

    Этот метод должен вернуть конкретный итератор соответствующего класса, связав его с текущим объектом коллекции.

  3. Итератор определяет интерфейс для доступа и обхода элементов коллекции.

  4. Конкретный итератор реализует интерфейс Итератора. Он знает о подробностях реализации класса конкретной коллекции, которую ему предстоит обходить.

    Объекты-итераторы должны отслеживать текущую позицию при обходе коллекции.

  5. Клиент работает со всеми объектами через общие интерфейсы Коллекции и Итератора. Так клиент не будет зависеть от конкретного класса итератора. Это позволит подменять типы итераторов, не трогая клиентский код.

    В большинстве случаев, Клиент не создаёт объекты итераторов, а получает их из Коллекций. Тем не менее, если клиенту требуется специальный итератор, он может его создать самостоятельно.

Псевдокод

В этом примере Итератор используется для реализации обхода нестандартной коллекции — профилей Facebook. Коллекция содержит несколько методов для получения различных итераторов. Каждый из этих итераторов может обходить элементы коллекции по-своему. Так, итератор друзей, будет перебирать всех друзей профиля, а итератор коллег — фильтровать друзей по принадлежности к компании профиля. Все итераторы реализуют простой общий интерфейс, который скрывает от клиентского кода сложность работы с социальной сетью (например, авторизацию, отправку REST запросов и т.д.)

Таким образом, паттерн Итератор позволяет скрыть от клиентского кода подробности реализации коллекции, предоставив простой интерфейс перебора её сущностей. Кроме того, Итератор избавляет код от привязки к конкретным классам коллекций. Это позволяет добавить поддержку другого вида коллекций (например, LinkedIn), не меняя существующий код.

// Общий интерфейс коллекций должен определить фабричный метод для производства
// итератора. Можно определить сразу несколько методов, чтобы дать пользователям
// различные варианты обхода одной и той же коллекции.
interface SocialNetwork is
    method getFriendsIterator(profileId): ProfileIterator
    method getCoworkerIterator(profileId): ProfileIterator

// Конкретная коллекция знает объекты каких итераторов нужно создавать.
class Facebook implements SocialNetwork is
    // Код получения нужного итератора.
    method getFriendsIterator(profileId) is
        return new FacebookIterator(this, profileId, "friends")
    method getCoworkerIterator(profileId) is
        return new FacebookIterator(this, profileId, "coworkers")
    // Но помните, что коллекция имеет и другой код...


// Общий интерфейс итераторов.
interface ProfileIterator is
    method hasNext(): bool
    method getNext(): Profile

// Конкретный итератор.
class FacebookIterator implements ProfileIterator is
    // Итератору нужна ссылка на коллекцию, которую он обходит.
    field facebook: Facebook
    field profileId, type: string

    // Но каждый итератор обходит коллекцию независимо от остальных, поэтому он
    // содержит информацию о текущей позиции обхода.
    field currentPosition
    field cache: array of Profile

    constructor FacebookIterator(facebook, profileId, type) is
        this.facebook = network
        this.profileId = profileId
        this.type = type

    private method initIfNeeded() is
        if (cache == null)
            cache = facebook.sendSophisticatedSocialGraphRequest(profileId, type)

    // Итератор реализует методы базового интерфейса по-своему.
    method hasNext() is
        initIfNeeded()
        return cache.length < currentPosition

    method getNext() is
        if (hasNext())
            currentPosition++;
            return cache[currentPosition]


// Вот ещё полезная тактика: мы можем передавать объект итератора вместо
// коллекции в клиентские классы. При таком подходе, клиентский код не будет
// иметь доступа к коллекциям, а значит его не будет волновать подробности их
// реализаций. Ему будет доступен только общий интерфейс итераторов.
class SocialSpammer is
    method send(iterator: ProfileIterator, message: string) is
        while (iterator.hasNext())
            profile = iterator.getNext()
            sendSingle(profile.email, message)

    method sendSingle(email: string, message: string) is
        // Выслать ОЧЕНЬ ВАЖНОЕ СООБЩЕНИЕ на один email.


// Класс приложение конфигурирует классы как захочет.
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() is
        iterator = network.getFriendsIterator(user.profileId)
        spammer.send(iterator)

    method sendSpamToCoworkers() is
        iterator = network.getCoworkerIterator(user.profileId)
        spammer.send(iterator)

Применимость

У вас есть сложная структура данных и вы хотите скрыть от пользователям детали её реализации (из-за сложности или вопросов безопасности).

Создайте итератор для простого доступа к своей коллекции. Он скроет детали структуры данных от клиента, а также защитит данные от неосторожных или злоумышленных действий.

Вам нужно иметь несколько вариантов обхода одной и той же структуры данных.

Нетривиальные способы обхода структуры данных могут захламлять бизнес-логику класса. В таком случае код обхода можно вынести в отдельные классы итераторов. Вы будете передавать определённый итератор в параметры операций бизнес-логики.

Вам хочется иметь единый интерфейс обхода различных структур данных.

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

Шаги реализации

  1. Создайте общий интерфейс Итераторов. Обычно там живут такие операции: первыйЭлемент, следующийЭлемент, текущийЭлемент, естьЛиЕщеЭлементы.

  2. Создайте интерфейс Коллекции и опишите в нём метод получитьИтератор, который должен возвращать объект Итератор.

  3. Создайте классы Конкретных итераторов для тех коллекций, в которых нужно обходить элементы.

  4. Реализуйте методы получитьИтератор в конкретных классах коллекций. Они должны создавать новый итератор того класса, который способен работать с данным типом коллекции. Коллекция должна передавать собственную ссылку в созданный итератор.

  5. В клиентском коде и в классах коллекций не должно остаться кода обхода элементов.

    Клиент должен будет получать новый итератор из объекта коллекции каждый раз когда ему нужно пройтись по элементам списка.

Преимущества и недостатки

  • Упрощает классы хранения данных.
  • Позволяет реализовать различные способы обхода структуры данных.
  • Позволяет одновременно перемещаться по структуре данных в разные стороны.
  • Неоправдан, если можно обойтись простым циклом.

Отношения с другими паттернами

  • Вы можете обходить дерево Компоновщика, используя Итератор.

  • Фабричный метод можно использовать вместе с Итератором, чтобы подклассы коллекций могли создавать подходящие им итераторы.

  • Снимок часто используется вместе с Итератором. Итератор может держать в снимке текущее состояние обхода своей структуры данных.

  • Посетитель можно использовать совместно с Итератором. Итератор будет отвечать за обход структуры данных, а Посетитель, за выполнение действий над каждым её компонентом.

Реализация в различных языках программирования

Java