Відвідувач і Double Dispatch
Розглянемо приклад, у якому ми маємо невелику ієрархію класів геометричних фігур (обережно, псевдокод):
Нам потрібно додати зовнішню оперцію над усіма цими компонентами, наприклад, експорт. У нашій мові (Java, C#, ...) є перевантаження методів, тому ми створюємо такий клас:
Здається, що все добре. Але давайте спробуємо такий клас на ділі:
Як? Але чому?!
Побувати в шкурі компілятора
Примітка: все, що тут описано — правдиве для більшості сучасних об’єктних мов програмування (Java, C#, PHP та інші).
Пізнє/динамічне зв’язування
Давайте уявімо себе компілятором. Вам потрібно зрозуміти, як скомпілювати такий код:
Отже, виклик метода draw
у класі Shape
. Але нам також відомо про чотири класи, що перевизначають цей метод. Чи можливо вже зараз зрозуміти, яку реалізацію потрібно вибрати? Схоже, що ні, адже для цього доведеться запустити програму й дізнатися, який саме об’єкт буде поданий у параметр. Але одне ви знаєте напевне — який би об’єкт не був переданий, він обов’язково матиме реалізацію draw
.
В кінцевому рахунку машинний код, який ви створите, під час переходу через цю ділянку буде щоразу перевіряти, що за об’єкт цей shape
, і вибирати реалізацію метода draw
із відповідного класа.
Така динамічна перевірка типу називається пізнім або динамічним зв’язуванням:
- Пізнім, тому що ми пов’язуємо об’єкт та реалізацію вже після компіляції.
- Динамічним, тому що ми робимо це під час кожного проходження через цю ділянку.
Раннє/статичне зв’язування
Тепер давайте «скомпілюємо» такий код:
Зі створенням об’єкту все зрозуміло. Як щодо виклику методу export
? У класі Exporter
у нас є п’ять версій методу з таким ім’ям, які відрізняються лише типом параметру. Схоже, що тут також доведеться динамічно відстежувати тип параметра, що передається, і за ним визначати, який з методів вибрати.
Але тут на нас чекає прикра несподіванка. Що як хто-небудь подасть у метод exportShape
такий об’єкт, для якого відсутній метод export
у класі Exporter
? Наприклад, об’єкт Ellipse
, для якого у нас немає експорту. Дійсно, ми не маємо гарантії, що необхідний метод буде присутній, як це було з перевизначеними методами. А отже, виникне неоднозначна ситуація.
Саме через це всі розробники компіляторів обирають безпечний шлях і використовують раннє або статичне зв’язування для перевантажених методів:
- Раннє, тому що воно відбувається ще на етапі компіляції програми.
- Статичне, тому що його вже не можна змінити під час виконання.
Повернемося до нашого прикладу. Ми впевнені в тому, що маємо параметр з типом Shape
. Ми знаємо, що в Exporter
є реалізація, яка підходить для цього: export(s: Shape)
. Відповідно, цю ділянку коду ми жорстко зв’язуємо з відомою реалізацією методу.
І тому навіть якщо ми подамо у параметрах один з підкласів Shape
, однаково буде викликана реалізація export(s: Shape)
.
Double dispatch
Подвійна диспетчеризація (або double dispatch) — це трюк, який дає змогу оминути обмеженість раннього зв’язування в перевантажених методах. Ось як це робиться:
Післяслово
Хоча патерн Відвідувач і побудований на механіці подвійної диспетчеризації, це не основна його ідея. Відвідувач дає змогу додавати операції до цілої ієрархії класів, без потреби міняти код цих класів.