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

Відвідувач

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

Суть патерна

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

Патерн Відвідувач

Проблема

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

Експорт гео-вузлів до XML

Експорт гео-вузлів до XML.

Ваше завдання — зробити експорт цього графа до XML. Справа була б легкою, якщо б ви могли редагувати класи вузлів. У цьому випадку можна було б додати метод експорту до кожного типу вузлів, а потім, перебираючи всі вузли графа, викликати цей метод для кожного вузла. Завдяки поліморфізму, рішення було б елегантним, оскільки ви могли б не прив’язуватися до конкретних класів вузлів.

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

Код XML-експорту доведеться додати до всіх класів вузлів

Код XML-експорту доведеться додати до всіх класів вузлів, а це дуже невигідно.

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

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

Рішення

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

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

class ExportVisitor implements Visitor is
    method doForCity(City c) { ... }
    method doForIndustry(Industry f) { ... }
    method doForSightSeeing(SightSeeing ss) { ... }
    // ...

Тут виникає запитання, яким чином ми будемо подавати вузли до об’єкта відвідувача. Оскільки усі методи відрізняються сигнатурою, використати поліморфізм при перебиранні вузлів не вийде. Доведеться перевіряти тип вузлів для того, щоб вибрати відповідний метод відвідувача.

foreach (Node node : graph)
    if (node instanceof City)
        exportVisitor.doForCity((City) node);
    if (node instanceof Industry)
        exportVisitor.doForIndustry((Industry) node);
    // ...

Тут не допоможе навіть механізм перевантаження методів (доступний у Java і C#). Якщо назвати всі методи однаково, то невизначеність реального типу вузла все одно не дасть викликати правильний метод. Механізм перевантаження весь час викликатиме метод відвідувача, відповідний типу Node, а не реального класу поданого вузла.

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

// Client code
foreach (Node node : graph)
    node.accept(exportVisitor);

// City
class City is
    method accept(Visitor v) is
        v.doForCity(this);
    // ...

// Industry
class Industry is
    method accept(Visitor v) is
        v.doForIndustry(this);
    // ...

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

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

Страховий агент

У страхового агента приготовані поліси для різних видів організацій.

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

  • Прийшовши до будинку звичайної сім’ї, він пропонує оформити медичну страховку.
  • Прийшовши до банку, він пропонує страховку на випадок пограбування.
  • Прийшовши на фабрику, він пропонує страхування підприємства на випадок пожежі чи повені.

Структура

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

  2. Конкретні відвідувачі реалізують якусь особливу поведінку для всіх типів елементів, які можна подати через методи інтерфейсу відвідувача.

  3. Елемент описує метод прийому відвідувача. Цей метод повинен мати лише один параметр, оголошений з типом загального інтерфейсу відвідувачів.

  4. Конкретні елементи реалізують методи приймання відвідувача. Мета цього методу — викликати той метод відвідування, який відповідає типу цього елемента. Так відвідувач дізнається, з яким типом елементу він працює.

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

Псевдокод

У цьому прикладі Відвідувач додає до існуючої ієрархії класів геометричних фігур можливість експорту до XML.

Структура класів прикладу патерна Відвідувач

Приклад організації експорту об’єктів XML через окремий клас-відвідувач.

// Складна ієрархія елементів.
interface Shape is
    method move(x, y)
    method draw()
    method accept(v: Visitor)

// Метод прийняття відвідувача повинен бути реалізований у
// кожному елементі, а не тільки у базовому класі. Це допоможе
// програмі визначити, який метод відвідувача потрібно викликати
// у випадку, якщо ви не знаєте тип елемента.
class Dot implements Shape is
    // ...
    method accept(v: Visitor) is
        v.visitDot(this)

class Circle implements Shape is
    // ...
    method accept(v: Visitor) is
        v.visitCircle(this)

class Rectangle implements Shape is
    // ...
    method accept(v: Visitor) is
        v.visitRectangle(this)

class CompoundShape implements Shape is
    // ...
    method accept(v: Visitor) is
        v.visitCompoundShape(this)


// Інтерфейс відвідувачів повинен містити методи відвідування
// кожного елемента. Важливо, щоб ієрархія елементів змінювалася
// рідко, оскільки при додаванні нового елемента доведеться
// змінювати всіх існуючих відвідувачів.
interface Visitor is
    method visitDot(d: Dot)
    method visitCircle(c: Circle)
    method visitRectangle(r: Rectangle)
    method visitCompoundShape(cs: CompoundShape)

// Конкретний відвідувач реалізує одну операцію для всієї
// ієрархії елементів. Нова операція = новий відвідувач.
// Відвідувача вигідно застосовувати, коли нові елементи
// додаються дуже зрідка, а нові операції — часто.
class XMLExportVisitor implements Visitor is
    method visitDot(d: Dot) is
        // Експорт id та координат центру точки.

    method visitCircle(c: Circle) is
        // Експорт id, координат центру та радіусу кола.

    method visitRectangle(r: Rectangle) is
        // Експорт id, координат лівого-верхнього кута, висоти
        // та ширини прямокутника.

    method visitCompoundShape(cs: CompoundShape) is
        // Експорт id складової фігури, а також списку id
        // підфігур, з яких вона складається.


// Програма може застосовувати відвідувача до будь-якого набору
// об'єктів елементів, навіть не уточнюючи їхні типи. Потрібний
// метод відвідувача буде обрано завдяки проходу через метод
// accept.
class Application is
    field allShapes: array of Shapes

    method export() is
        exportVisitor = new XMLExportVisitor()

        foreach (shape in allShapes) do
            shape.accept(exportVisitor)

Вам не здається, що виклик методу accept — це зайва ланка? Якщо так, тоді ще раз рекомендую вам ознайомитися з проблемою раннього та пізнього зв’язування в статті Відвідувач і Double Dispatch.

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

Якщо вам потрібно виконати якусь операцію над усіма елементами складної структури об’єктів, наприклад, деревом.

Відвідувач дозволяє застосовувати одну і ту саму операцію до об’єктів різних класів.

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

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

Якщо нова поведінка має сенс тільки для деяких класів з існуючої ієрархії.

Відвідувач дозволяє визначити поведінку тільки для цих класів, залишивши її порожньою для всіх інших.

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

  1. Створіть інтерфейс відвідувача й оголосіть у ньому методи «відвідування» для кожного класу елемента, який існує в програмі.

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

  3. Реалізуйте методи прийняття в усіх конкретних елементах. Вони повинні переадресовувати виклики тому методу відвідувача, в якому тип параметра збігається з поточним класом елемента.

  4. Ієрархія елементів повинна знати тільки про загальний інтерфейс відвідувачів. З іншого боку, відвідувачі знатимуть про всі класи елементів.

  5. Для кожної нової поведінки створіть свій власний конкретний клас. Пристосуйте цю поведінку для роботи з усіма наявними типами елементів, реалізувавши всі методи інтерфейсу відвідувачів.

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

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

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

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

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

  • Відвідувач можна розглядати як розширений аналог Команди, що здатен працювати відразу з декількома видами одержувачів.

  • Ви можете виконати якусь дію над усім деревом Компонувальника за допомогою Відвідувача.

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

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

Відвідувач на C# Відвідувач на C++ Відвідувач на Go Відвідувач на Java Відвідувач на PHP Відвідувач на Python Відвідувач на Ruby Відвідувач на Rust Відвідувач на Swift Відвідувач на TypeScript

Додаткові матеріали

  • Детальніше про те, чому Відвідувача не можна замінити простим перевантаженням методів, читайте у статті Відвідувач і Double Dispatch.