Відвідувач
Суть патерна
Відвідувач — це поведінковий патерн проектування, що дає змогу додавати до програми нові операції, не змінюючи класи об’єктів, над якими ці операції можуть виконуватися.
Проблема
Ваша команда розробляє програму, що працює з геоданими у вигляді графа. Вузлами графа можуть бути як міста, так інші локації, такі, як пам’ятки, великі підприємства тощо. Кожен вузол має посилання на найближчі до нього вузли. Для кожного типу вузла існує свій власний клас, а кожен вузол представлений окремим об’єктом.
Ваше завдання — зробити експорт цього графа до XML. Справа була б легкою, якщо б ви могли редагувати класи вузлів. У цьому випадку можна було б додати метод експорту до кожного типу вузлів, а потім, перебираючи всі вузли графа, викликати цей метод для кожного вузла. Завдяки поліморфізму, рішення було б елегантним, оскільки ви могли б не прив’язуватися до конкретних класів вузлів.
Але, на жаль, змінити класи вузлів у вас не вийшло. Системний архітектор сказав, що код класів вузлів зараз дуже стабільний, і від нього багато що залежить, а тому він не хоче ризикувати, дозволяючи будь-кому чіпати цей код.
До того ж він сумнівався в тому, що експорт до XML взагалі є доречним в рамках цих класів. Їхнє основне завдання пов’язане з геоданими, а експорт виглядає в межах цих класів, як біла ворона.
Була ще одна причина заборони. Наступного тижня вам міг знадобитися експорт в який-небудь інший формат даних, а це призвело б до повторних змін в класах.
Рішення
Патерн Відвідувач пропонує розмістити нову поведінку в окремому класі, замість того, щоб множити її відразу в декількох класах. Об’єкти, з якими повинна бути пов’язана поведінка, не виконуватимуть її самостійно. Замість цього ви будете передавати ці об’єкти до методів відвідувача.
Код поведінки, імовірно, повинен відрізнятися для об’єктів різних класів, тому й методів у відвідувача повинно бути декілька. Назви та принцип дії цих методів будуть подібними, а основна відмінність торкатиметься типу, що приймається в параметрах об’єкта, наприклад:
Тут виникає запитання, яким чином ми будемо подавати вузли до об’єкта відвідувача. Оскільки усі методи відрізняються сигнатурою, використати поліморфізм при перебиранні вузлів не вийде. Доведеться перевіряти тип вузлів для того, щоб вибрати відповідний метод відвідувача.
Тут не допоможе навіть механізм перевантаження методів (доступний у Java і C#). Якщо назвати всі методи однаково, то невизначеність реального типу вузла все одно не дасть викликати правильний метод. Механізм перевантаження весь час викликатиме метод відвідувача, відповідний типу Node
, а не реального класу поданого вузла.
Але патерн Відвідувач вирішує і цю проблему, використовуючи механізм подвійної диспетчеризації. Замість того, щоб самим шукати потрібний метод, ми можемо доручити це об’єктам, які передаємо в параметрах відвідувачеві, а вони вже самостійно викличуть правильний метод відвідувача.
Як бачите, змінити класи вузлів все-таки доведеться. Проте ця проста зміна дозволить застосувати до об’єктів вузлів й інші поведінки, адже класи вузлів будуть прив’язані не до конкретного класу відвідувачів, а до їхнього загального інтерфейсу. Тому, якщо доведеться додати до програми нову поведінку, ви створите новий клас відвідувачів і будете передавати його до методів вузлів.
Аналогія з життя
Уявіть собі страхового агента-початківця, який прагне отримати нових клієнтів. Він хаотично відвідує всі будинки навколо, пропонуючи свої послуги. Але для кожного типу будинків, які він відвідує, у нього є особлива пропозиція.
- Прийшовши до будинку звичайної сім’ї, він пропонує оформити медичну страховку.
- Прийшовши до банку, він пропонує страховку на випадок пограбування.
- Прийшовши на фабрику, він пропонує страхування підприємства на випадок пожежі чи повені.
Структура
-
Відвідувач описує спільний для всіх типів відвідувачів інтерфейс. Він оголошує набір методів, що відрізняються типом вхідного параметра. Кожному класу конкретних елементів повинен підходити свій метод. В мовах, які підтримують перевантаження методів, ці методи можуть мати однакові імена, але типи їхніх параметрів повинні відрізнятися.
-
Конкретні відвідувачі реалізують якусь особливу поведінку для всіх типів елементів, які можна подати через методи інтерфейсу відвідувача.
-
Елемент описує метод прийому відвідувача. Цей метод повинен мати лише один параметр, оголошений з типом загального інтерфейсу відвідувачів.
-
Конкретні елементи реалізують методи приймання відвідувача. Мета цього методу — викликати той метод відвідування, який відповідає типу цього елемента. Так відвідувач дізнається, з яким типом елементу він працює.
-
Клієнтом зазвичай виступає колекція або складний складовий об’єкт, наприклад, дерево Компонувальника. Здебільшого, клієнт не прив’язаний до конкретних класів елементів, працюючи з ними через загальний інтерфейс елементів.
Псевдокод
У цьому прикладі Відвідувач додає до існуючої ієрархії класів геометричних фігур можливість експорту до XML.
Вам не здається, що виклик методу accept
— це зайва ланка? Якщо так, тоді ще раз рекомендую вам ознайомитися з проблемою раннього та пізнього зв’язування в статті Відвідувач і Double Dispatch.
Застосування
Якщо вам потрібно виконати якусь операцію над усіма елементами складної структури об’єктів, наприклад, деревом.
Відвідувач дозволяє застосовувати одну і ту саму операцію до об’єктів різних класів.
Якщо над об’єктами складної структури об’єктів потрібно виконувати деякі не пов’язані між собою операції, але ви не хочете «засмічувати» класи такими операціями.
Відвідувач дозволяє витягти споріднені операції з класів, що складають структуру об’єктів, помістивши їх до одного класу-відвідувача. Якщо структура об’єктів використовується в декількох програмах, то патерн дозволить кожній програмі мати тільки потрібні в ній операції.
Якщо нова поведінка має сенс тільки для деяких класів з існуючої ієрархії.
Відвідувач дозволяє визначити поведінку тільки для цих класів, залишивши її порожньою для всіх інших.
Кроки реалізації
-
Створіть інтерфейс відвідувача й оголосіть у ньому методи «відвідування» для кожного класу елемента, який існує в програмі.
-
Опишіть інтерфейс елементів. Якщо ви працюєте з уже існуючими класами, оголосіть абстрактний метод прийняття відвідувачів у базовому класі ієрархії елементів.
-
Реалізуйте методи прийняття в усіх конкретних елементах. Вони повинні переадресовувати виклики тому методу відвідувача, в якому тип параметра збігається з поточним класом елемента.
-
Ієрархія елементів повинна знати тільки про загальний інтерфейс відвідувачів. З іншого боку, відвідувачі знатимуть про всі класи елементів.
-
Для кожної нової поведінки створіть свій власний конкретний клас. Пристосуйте цю поведінку для роботи з усіма наявними типами елементів, реалізувавши всі методи інтерфейсу відвідувачів.
Ви можете зіткнутися з ситуацією, коли відвідувачу потрібен доступ до приватних полів елементів. У цьому випадку ви можете або розкрити доступ до цих полів, порушивши інкапсуляцію елементів, або зробити клас відвідувача вкладеним в клас елемента, якщо вам пощастило писати мовою, яка підтримує механізм вкладених класів.
-
Клієнт створюватиме об’єкти відвідувачів, а потім передаватиме їх елементам через метод прийняття.
Переваги та недоліки
- Спрощує додавання операцій, працюючих зі складними структурами об’єктів.
- Об’єднує споріднені операції в одному класі.
- Відвідувач може накопичувати стан при обході структури елементів.
- Патерн невиправданий, якщо ієрархія елементів часто змінюється.
- Може призвести до порушення інкапсуляції елементів.
Відносини з іншими патернами
-
Відвідувач можна розглядати як розширений аналог Команди, що здатен працювати відразу з декількома видами одержувачів.
-
Ви можете виконати якусь дію над усім деревом Компонувальника за допомогою Відвідувача.
-
Відвідувач можна використовувати спільно з Ітератором. Ітератор відповідатиме за обхід структури даних, а Відвідувач — за виконання дій над кожним її компонентом.
Додаткові матеріали
- Детальніше про те, чому Відвідувача не можна замінити простим перевантаженням методів, читайте у статті Відвідувач і Double Dispatch.