Візерунок відвідувача visit
/accept
конструкція відвідувача є необхідним злом через семантику подібних до C мов (C #, Java тощо). Мета шаблону відвідувача - використовувати подвійну розсилку для маршрутизації вашого дзвінка, як ви очікували від прочитання коду.
Зазвичай, коли використовується шаблон відвідувача, задіяна ієрархія об'єкта, де всі вузли походять від базового Node
типу, і надалі іменованого Node
. Ми інстинктивно писали б це так:
Node root = GetTreeRoot();
new MyVisitor().visit(root);
У цьому полягає проблема. Якби наш MyVisitor
клас визначався так:
class MyVisitor implements IVisitor {
void visit(CarNode node);
void visit(TrainNode node);
void visit(PlaneNode node);
void visit(Node node);
}
Якщо під час виконання, незалежно від фактичного типу, який root
є, наш дзвінок перейде в перевантаження visit(Node node)
. Це було б справедливо для всіх змінних, оголошених типом Node
. Чому це? Оскільки Java та інші мови, подібні до C, враховують лише статичний тип або тип, яким змінна оголошена, параметра, приймаючи рішення, яке перевантаження викликати. Java не робить зайвого кроку, щоб запитати, під час кожного виклику методу, під час виконання, "Гаразд, що таке динамічний тип root
? О, я розумію. Це a TrainNode
. Давайте подивимось, чи є якийсь метод уMyVisitor
який приймає параметр типуTrainNode
... ". Компілятор під час компіляції визначає, який саме метод буде викликаний. (Якби Java справді перевіряла динамічні типи аргументів, продуктивність була б досить жахливою.)
Java дійсно дає нам один інструмент для врахування часу виконання (тобто динамічного) типу об'єкта при виклику методу - віртуальне відправлення методу . Коли ми викликаємо віртуальний метод, виклик фактично надходить до таблиці в пам'яті, яка складається з покажчиків на функції. Кожен тип має таблицю. Якщо певний метод замінений класом, запис таблиці функцій цього класу міститиме адресу заміненої функції. Якщо клас не замінює метод, він міститиме вказівник на реалізацію базового класу. Це все одно спричиняє накладні витрати на продуктивність (кожен виклик методу в основному буде розмежовувати два покажчики: один, що вказує на таблицю функцій типу, а інший на саму функцію), але це все одно швидше, ніж перевірка типів параметрів.
Метою шаблону відвідувача є досягнення подвійної розсилки - враховується не тільки тип цільового виклику ( MyVisitor
за допомогою віртуальних методів), але і тип параметра (який тип Node
ми розглядаємо)? Шаблон відвідувача дозволяє нам робити це за допомогою visit
/ accept
комбінації.
Змінивши наш рядок на такий:
root.accept(new MyVisitor());
Ми можемо отримати те, що хочемо: за допомогою віртуального відправлення методу ми вводимо правильний виклик accept (), як реалізовано підкласом - у нашому прикладі з TrainElement
, ми введемо TrainElement
реалізацію accept()
:
class TrainNode extends Node implements IVisitable {
void accept(IVisitor v) {
v.visit(this);
}
}
Що знає компілятор на даний момент, у межах області TrainNode
's accept
? Він знає, що статичним типом this
є aTrainNode
. Це важливий додатковий фрагмент інформації, про який компілятор не знав у межах нашої телефонної лінії: там він знав лише те, root
що це був Node
. Тепер компілятор знає, що this
( root
) - це не просто a Node
, а насправді a TrainNode
. Як наслідок, один рядок, що знаходиться всередині accept()
:, v.visit(this)
означає зовсім інше. Тепер компілятор шукатиме перевантаження, visit()
яке займає TrainNode
. Якщо він не може знайти його, він компілює виклик до перевантаження, яке займаєNode
. Якщо жодне з них не існує, ви отримаєте помилку компіляції (якщо у вас не виникає перевантаження object
). Таким чином, виконання включатиме те, що ми передбачали весь час: MyVisitor
реалізацію visit(TrainNode e)
. Не потрібні були акторські склади, і, головне, не потрібні були роздуми. Таким чином, накладні витрати на цей механізм є досить низькими: він складається лише з посилань на вказівники та нічого іншого.
Ви маєте рацію у своєму питанні - ми можемо використовувати гіпс і отримати правильну поведінку. Однак часто ми навіть не знаємо, що таке тип Node. Візьмемо до уваги наступну ієрархію:
abstract class Node { ... }
abstract class BinaryNode extends Node { Node left, right; }
abstract class AdditionNode extends BinaryNode { }
abstract class MultiplicationNode extends BinaryNode { }
abstract class LiteralNode { int value; }
І ми писали простий компілятор, який аналізує вихідний файл і створює ієрархію об’єктів, яка відповідає вищевказаній специфікації. Якби ми писали інтерпретатор для ієрархії, реалізованої як Відвідувач:
class Interpreter implements IVisitor<int> {
int visit(AdditionNode n) {
int left = n.left.accept(this);
int right = n.right.accept(this);
return left + right;
}
int visit(MultiplicationNode n) {
int left = n.left.accept(this);
int right = n.right.accept(this);
return left * right;
}
int visit(LiteralNode n) {
return n.value;
}
}
Кастинг не отримали б нас дуже далеко, так як ми не знаємо , типів left
або right
в visit()
методах. Наш синтаксичний аналізатор також, швидше за все, також просто поверне об’єкт типу, Node
який також вказував на корінь ієрархії, тому ми також не можемо це безпечно передати. Тож наш простий перекладач може виглядати так:
Node program = parse(args[0]);
int result = program.accept(new Interpreter());
System.out.println("Output: " + result);
Шаблон відвідувача дозволяє нам зробити щось дуже потужне: враховуючи ієрархію об’єктів, він дозволяє нам створювати модульні операції, які діють над ієрархією, не вимагаючи введення коду в сам клас ієрархії. Шаблон відвідувача широко використовується, наприклад, у побудові компілятора. Враховуючи дерево синтаксису конкретної програми, багато відвідувачів пишуть, що працюють з цим деревом: перевірка типу, оптимізація, емісія машинного коду, як правило, реалізуються як різні відвідувачі. У випадку відвідувача оптимізації, він може навіть вивести нове дерево синтаксису, враховуючи дерево введення.
Звичайно, він має свої недоліки: якщо ми додаємо новий тип в ієрархію, нам також потрібно додати visit()
метод для цього нового типу в IVisitor
інтерфейс і створити реалізації стабілізатора (або повні) у всіх наших відвідувачів. Нам також потрібно додати accept()
метод теж, з причин, описаних вище. Якщо продуктивність для вас не означає так багато, є рішення для написання відвідувачів без необхідності accept()
, але вони зазвичай передбачають роздуми і, отже, можуть спричинити досить великі накладні витрати.