Також відомий як: 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 extends Shape is
    // ...
    method accept(v: Visitor) is
        v.visitDot(this)

class Circle extends Dot is
    // ...
    method accept(v: Visitor) is
        v.visitCircle(this)

class Rectangle extends 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 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. Клієнт створюватиме об'єкти відвідувачів, а потім передаватиме їх компонентам через метод прийняття.

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

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

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

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

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

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

Реалізація в різних мовах програмування

Java

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

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