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

Итератор

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

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

Проблема

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

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

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

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

Решение

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

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

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

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

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

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

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

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

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

Структура

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

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

  3. Коллекция описывает интерфейс получения итератора из коллекции. Как мы уже говорили, коллекции не всегда являются списком. Это может быть и база данных, и удалённое API, и даже дерево Компоновщика. Поэтому сама коллекция может создавать итераторы, так как она знает какие из конкретных итераторов могут с ней работать.

  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