Весняний РОЗПРОДАЖ

Ітератор

Також відомий як: Iterator

Суть патерна

Ітератор — це поведінковий патерн проектування, що дає змогу послідовно обходити елементи складових об’єктів, не розкриваючи їхньої внутрішньої організації.

Патерн Ітератор

Проблема

Колекції — це найпоширеніша структура даних, яку ви можете зустріти в програмуванні. Це набір об’єктів, зібраний в одну купу за якимись критеріями.

Різні типи колекцій

Різні типи колекцій.

Більшість колекцій виглядають як звичайний список елементів. Але є й екзотичні колекції, побудовані на основі дерев, графів та інших складних структур даних.

Незважаючи на те, яким чином структуровано колекцію, користувач повинен мати можливість послідовно обходити її елементи, щоб виконувати з ними певні дії.

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

Одну і ту саму колекцію можна обходити різними способами

Одну і ту саму колекцію можна обходити різними способами.

Додаючи все нові алгоритми до коду колекції, ви потроху розмиваєте її основну задачу, що полягає в ефективному зберіганні даних. Деякі алгоритми можуть бути аж занадто «заточені» під певну програму, а тому виглядатимуть неприродно в загальному класі колекції.

Рішення

Ідея патерна Ітератор полягає в тому, щоб винести поведінку обходу колекції з самої колекції в окремий об’єкт.

Ітератори містять код обходу колекції

Ітератори містять код обходу колекції. Одну колекцію можуть обходити відразу декілька ітераторів.

Об’єкт-ітератор відстежуватиме стан обходу, поточну позицію в колекції та кількість елементів, які ще залишилося обійти. Одну і ту саму колекцію зможуть одночасно обходити різні ітератори, а сама колекція навіть не знатиме про це.

До того ж, якщо вам потрібно буде додати новий спосіб обходу, ви зможете створите окремий клас ітератора, не змінюючи існуючого коду колекції.

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

Варіанти прогулянок Римом

Варіанти прогулянок Римом.

Ви плануєте полетіти до Риму та обійти всі визначні пам’ятки за кілька днів. Але по приїзді ви можете довго блукати вузькими вуличками, намагаючись знайти один тільки Колізей.

Якщо у вас обмежений бюджет, ви можете скористатися віртуальним гідом, встановленим у смартфоні, який дозволить відфільтрувати тільки цікаві вам об’єкти. А можете плюнути на все та найняти місцевого гіда, який хоч і обійдеться в копієчку, але знає все місто, як свої п’ять пальців, і зможе «занурити» вас в усі міські легенди.

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

Структура

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

  2. Конкретний ітератор реалізує алгоритм обходу якоїсь конкретної колекції. Об’єкт ітератора повинен сам відстежувати поточну позицію при обході колекції, щоб окремі ітератори могли обходити одну і ту саму колекцію незалежно.

  3. Колекція описує інтерфейс отримання ітератора з колекції. Як ми вже говорили, колекції не завжди є списком. Це може бути і база даних, і віддалене API, і навіть дерево Компонувальника. Тому сама колекція може створювати ітератори, оскільки вона знає, які саме ітератори здатні з нею працювати.

  4. Конкретна колекція повертає новий екземпляр певного конкретного ітератора, зв’язавши його з поточним об’єктом колекції. Зверніть увагу на те, що сигнатура методу повертає інтерфейс ітератора. Це дозволяє клієнтові не залежати від конкретних класів ітераторів.

  5. Клієнт працює з усіма об’єктами через інтерфейси колекції та ітератора. Через це клієнтський код не залежить від конкретних класів, що дозволяє застосовувати різні ітератори, не змінюючи існуючого коду програми.

    В загальному випадку клієнти не створюють об’єкти ітераторів, а отримують їх з колекцій. Тим не менше, якщо клієнтові потрібний спеціальний ітератор, він завжди може створити його самостійно.

Псевдокод

У цьому прикладі патерн Ітератор використовується для реалізації обходу нестандартної колекції, яка інкапсулює доступ до соціального графа 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")

Застосування

Якщо у вас є складна структура даних, і ви хочете приховати від клієнта деталі її реалізації (з питань складності або безпеки).

Ітератор надає клієнтові лише кілька простих методів перебору елементів колекції. Це не тільки спрощує доступ до колекції, але й захищає її від необережних або злочинних дій.

Якщо вам потрібно мати кілька варіантів обходу однієї і тієї самої структури даних.

Нетривіальні алгоритми обходу структури даних можуть мати досить об’ємний код. Цей код буде захаращувати все навкруги — чи то самий клас колекції, чи частина бізнес-логіки програми. Застосувавши ітератор, ви можете виділити код обходу структури даних в окремий клас, спростивши підтримку решти коду.

Якщо вам хочеться мати єдиний інтерфейс обходу різних структур даних.

Ітератор дозволяє винести реалізації різних варіантів обходу в підкласи. Це дозволить легко взаємозаміняти об’єкти ітераторів в залежності від того, з якою структурою даних доводиться працювати.

Кроки реалізації

  1. Створіть загальний інтерфейс ітераторів. Обов’язковий мінімум — це операція отримання наступного елемента. Але для зручності можна передбачити й інше. Наприклад, методи отримання попереднього елементу, поточної позиції, перевірки закінчення обходу тощо.

  2. Створіть інтерфейс колекції та опишіть у ньому метод отримання ітератора. Важливо, щоб сигнатура методу повертала загальний інтерфейс ітераторів, а не один з конкретних ітераторів.

  3. Створіть класи конкретних ітераторів для тих колекцій, які потрібно обходити за допомогою патерна. Ітератор повинен бути прив’язаний тільки до одного об’єкта колекції. Зазвичай цей зв’язок встановлюється через конструктор.

  4. Реалізуйте методи отримання ітератора в конкретних класах колекцій. Вони повинні створювати новий ітератор того класу, який здатен працювати з даним типом колекції. Колекція повинна передавати посилання на власний об’єкт до конструктора ітератора.

  5. У клієнтському коді та в класах колекцій не повинно залишитися коду обходу елементів. Клієнт повинен отримувати новий ітератор з об’єкта колекції кожного разу, коли йому потрібно перебрати її елементи.

Переваги та недоліки

  • Спрощує класи зберігання даних.
  • Дозволяє реалізувати різні способи обходу структури даних.
  • Дозволяє одночасно переміщуватися структурою даних у різних напрямках.
  • Невиправданий, якщо можна обійтися простим циклом.

Відносини з іншими патернами

  • Ви можете обходити дерево Компонувальника, використовуючи Ітератор.

  • Фабричний метод можна використовувати разом з Ітератором, щоб підкласи колекцій могли створювати необхідні їм ітератори.

  • Знімок можна використовувати разом з Ітератором, щоб зберегти поточний стан обходу структури даних та повернутися до нього в майбутньому, якщо буде потрібно.

  • Відвідувач можна використовувати спільно з Ітератором. Ітератор відповідатиме за обхід структури даних, а Відвідувач — за виконання дій над кожним її компонентом.

Приклади реалізації патерна

Ітератор на C# Ітератор на C++ Ітератор на Go Ітератор на Java Ітератор на PHP Ітератор на Python Ітератор на Ruby Ітератор на Rust Ітератор на Swift Ітератор на TypeScript