У чому сенс методу accept () у шаблоні відвідувача?


87

Багато розмов ведеться про роз'єднання алгоритмів із класами. Але одне залишається осторонь не поясненим.

Вони використовують такого відвідувача

abstract class Expr {
  public <T> T accept(Visitor<T> visitor) {visitor.visit(this);}
}

class ExprVisitor extends Visitor{
  public Integer visit(Num num) {
    return num.value;
  }

  public Integer visit(Sum sum) {
    return sum.getLeft().accept(this) + sum.getRight().accept(this);
  }

  public Integer visit(Prod prod) {
    return prod.getLeft().accept(this) * prod.getRight().accept(this);
  }

Замість того, щоб безпосередньо викликати візит (елемент), відвідувач просить елемент викликати метод відвідування. Це суперечить заявленій ідеї несвідомості класу про відвідувачів.

PS1 Будь ласка, поясніть своїми словами або вкажіть точне пояснення. Оскільки дві відповіді, які я отримав, стосуються чогось загального та невизначеного.

PS2 Моє припущення: Оскільки getLeft()повернення базового Expression, виклик visit(getLeft())призведе до visit(Expression), тоді як getLeft()виклик visit(this)призведе до іншого, більш доцільного, виклику відвідування. Отже, accept()виконує перетворення типу (він же кастинг).

PS3 Scala's Pattern Matching = шаблон відвідувача на стероїді показує, наскільки шаблон відвідувача простіший без методу accept. Вікіпедія додає до цього твердження : шляхом посилання на статтю, яка показує, "що accept()методи непотрібні, коли доступні роздуми; вводить термін" Walkabout "для техніки".



У ньому сказано: "коли відвідувач дзвонить прийняти, виклик надсилається залежно від типу абонента. Потім виклик викликає зворотний метод відвідувача за типом відвідувача, і цей виклик відправляється на основі фактичного типу відвідувача". Іншими словами, там сказано, що мене бентежить. З цієї причини, будь ласка, будьте більш конкретними?
Валь

Відповіді:


154

Візерунок відвідувача 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(), але вони зазвичай передбачають роздуми і, отже, можуть спричинити досить великі накладні витрати.


5
Ефективний пункт Java № 41 включає таке застереження: " уникайте ситуацій, коли однаковий набір параметрів може передаватися різним перевантаженням шляхом додавання закидів. " accept()Метод стає необхідним, коли це застереження порушується у Відвідувачі.
jaco0646

" Зазвичай, коли використовується шаблон відвідувача, бере участь ієрархія об'єктів, де всі вузли походять від базового типу Вузла ", це абсолютно не потрібно в C ++. Дивіться Boost.Variant, Eggs.Variant
Jean-Michaël Celerier

Мені здається, що в Java нам насправді не потрібен метод accept, оскільки в Java ми завжди називаємо найбільш конкретний метод типу
Gilad Baruchian

1
Ого, це було приголомшливе пояснення. Просвітницьке побачити, що всі тіні візерунка пов’язані з обмеженнями компілятора, і тепер виявляється чітко завдяки вам.
Альфонсо Нісікава

@GiladBaruchian, компілятор генерує виклик найбільш конкретного методу типу, який компілятор може визначити.
MMW

15

Звичайно, це було б безглуздо, якби це єдиний спосіб реалізації Accept.

Але це не так.

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

interface IAcceptVisitor<T> {
  void Accept(IVisit<T> visitor);
}
class HierarchyNode : IAcceptVisitor<HierarchyNode> {
  public void Accept(IVisit<T> visitor) {
    visitor.visit(this);
    foreach(var n in this.children)
      n.Accept(visitor);
  }

  private IEnumerable<HierarchyNode> children;
  ....
}

Розумієш? Те, що ви описуєте як дурне, - це рішення для обходу ієрархій.

Ось набагато довша і глибша стаття, яка змусила мене зрозуміти відвідувача .

Редагувати: Для уточнення: Visitметод відвідувача містить логіку, яка застосовується до вузла. Метод вузла Acceptмістить логіку щодо переходу до сусідніх вузлів. Випадок, коли ви здійснюєте лише подвійне відправлення, - це особливий випадок, коли просто немає сусідніх вузлів, до яких можна перейти.


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

1
Сказати, що прийняти добре для рутинних обходів, є розумним і вартим для загальної популяції. Але я взяв свій приклад із чужого "Я не міг зрозуміти шаблон відвідувачів, поки не прочитав andymaleh.blogspot.com/2008/04/… ". Ні в цьому прикладі, ні у Вікіпедії, ні в інших відповідях не згадується перевага навігації. Тим не менше, всі вони вимагають цього дурного прийняття (). Ось чому задайте моє запитання: Чому?
Валь

1
@Val - що ти маєш на увазі? Я не впевнений, що ви запитуєте. Я не можу висловлюватися за інші статті, оскільки ці люди по-різному поглядають на це, але я сумніваюся, що ми не згодні. Загалом, при обчисленні багато проблем можна відобразити в мережах, тому використання може не мати нічого спільного з графіками на поверхні, але насправді є дуже подібною проблемою.
George Mauer

1
Надання прикладу того, де якийсь метод може бути корисним, не дає відповіді на питання, чому метод є обов’язковим. Оскільки навігація потрібна не завжди, метод accept () не завжди підходить для відвідування. Отже, ми повинні мати змогу досягти своїх цілей без цього. Тим не менше, це обов’язково. Це означає, що є більш вагома причина для введення accept () у кожен шаблон відвідувача, ніж "це іноді корисно". Що не зрозуміло в моєму питанні? Якщо ви не намагаєтеся зрозуміти, чому Вікіпедія шукає способів позбутися прийняття, вам не цікаво розуміти моє запитання.
Валь

1
@Val У статті, яку вони посилають на "Сутність візитника", в резюме зазначено те саме розділення навігації та експлуатації, що і я. Вони просто кажуть, що реалізація GOF (саме про це ви запитуєте) має деякі обмеження та неприємності, які можна усунути за допомогою відображення - тому вони вводять шаблон Walkabout. Це, безумовно, корисно і може зробити майже те саме, що може зробити відвідувач, але це багато досить витонченого коду і (при побіжному прочитанні) втрачає деякі переваги безпеки типу. Це інструмент для набору інструментів, але більш важкий, ніж відвідувач
Джордж Мауер

0

Призначення шаблону Відвідувач полягає в тому, щоб об'єкти знали, коли відвідувач закінчив з ними і відійшов, щоб класи змогли виконати будь-яке необхідне очищення після цього. Це також дозволяє класам виставляти свої внутрішні елементи "тимчасово" як параметри "ref", і знати, що внутрішні органи більше не будуть виставлені після того, як відвідувача не буде. У тих випадках, коли не потрібно очищення, шаблон відвідувачів не дуже корисний. Класи, які не виконують жодного з цих речей, можуть не отримати вигоду від шаблону відвідувача, але код, який написаний для використання шаблону відвідувача, буде використаний для майбутніх класів, які можуть вимагати очищення після доступу.

Наприклад, припустимо, що в одній структурі даних є багато рядків, які слід оновлювати атомарно, але клас, що містить структуру даних, не знає точно, які типи атомних оновлень слід виконувати (наприклад, якщо один потік хоче замінити всі випадки " X ", в той час як інший потік хоче замінити будь-яку послідовність цифр на послідовність, яка є чисельно на одну вищу, операції обох потоків повинні мати успіх; якщо кожен потік просто зчитує рядок, виконує оновлення та записує назад, другий потік щоб записати назад його рядок перезапише перший). Одним із способів досягти цього було б, щоб кожна нитка придбала замок, виконала свою операцію та звільнила замок. На жаль, якщо замки виставляються таким чином,

Шаблон відвідувача пропонує (принаймні) три підходи, щоб уникнути цієї проблеми:

  1. Він може заблокувати запис, викликати поставлену функцію, а потім розблокувати запис; запис може бути заблокований назавжди, якщо надана функція потрапляє в нескінченний цикл, але якщо надана функція повертає або видає виняток, запис буде розблоковано (може бути розумно позначити запис недійсним, якщо функція видає виняток; залишаючи він заблокований, мабуть, не є гарною ідеєю). Зверніть увагу, що важливо, якщо викликана функція намагається отримати інші блокування, може виникнути тупиковий стан.
  2. На деяких платформах він може передавати місце зберігання, що містить рядок як параметр 'ref'. Потім ця функція може скопіювати рядок, обчислити новий рядок на основі скопійованого рядка, спробувати CompareExchange старий рядок на новий і повторити весь процес, якщо CompareExchange не вдається.
  3. Він може зробити копію рядка, викликати надану функцію на рядку, потім використати саму CompareExchange, щоб спробувати оновити оригінал, і повторити весь процес, якщо CompareExchange не вдається.

Без шаблону відвідувача виконання атомних оновлень вимагало б викриття блокувань та ризику відмови, якщо виклик програмного забезпечення не дотримується суворого протоколу блокування / розблокування. За допомогою шаблону Visitor атомні оновлення можна робити відносно безпечно.


2
1. відвідування означає, що ви маєте доступ лише до загальнодоступних методів відвідування, тому потрібно зробити внутрішні замки доступними для загального користування, щоб бути корисними для відвідувача. 2 / Жоден з прикладів, які я бачив раніше, не означає, що відвідувач повинен використовуватися для зміни статусу відвідуваності. 3. "За допомогою традиційного VisitorPattern можна визначити лише, коли ми входимо у вузол. Ми не знаємо, чи залишили ми попередній вузол до того, як ми увійшли до поточного вузла." Як розблокувати лише відвідуванням замість visitEnter та visitLeave? Нарешті, я запитав про програми accpet (), а не Visitor.
Валь

Можливо, я не зовсім в курсі термінології для шаблонів, але "шаблон відвідувача", схоже, схожий на підхід, який я використовував, коли X передає Y делегату, якому Y може потім передавати інформацію, яка лише повинна бути дійсною як поки делегат працює. Можливо, цей зразок має якусь іншу назву?
supercat

2
Це цікаве застосування шаблону відвідувача до конкретної проблеми, але не описує сам шаблон і не відповідає на вихідне запитання. "У тих випадках, коли не потрібно очищення, шаблон відвідувача не надто корисний". Це твердження, безумовно, є помилковим і стосується лише вашої конкретної проблеми, а не шаблону загалом.
Тоні О'Хеган

0

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

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


-1

добре приклад у вихідному коді компіляції:

interface CompilingVisitor {
   build(SourceFile source);
}

Клієнти можуть застосувати a JavaBuilder , RubyBuilder,XMLValidator і т.д. , і здійснення збору і відвідування всіх вихідних файлів в проекті не вимагається змін.

Це було б погано шаблоном, якщо у вас є окремі класи для кожного типу вихідного файлу:

interface CompilingVisitor {
   build(JavaSourceFile source);
   build(RubySourceFile source);
   build(XMLSourceFile source);
}

Це зводиться до контексту та того, які частини системи ви хочете розширювати.


Іронія полягає в тому, що VisitorPattern пропонує нам використовувати поганий шаблон. Там сказано, що ми повинні визначити метод відвідування для кожного виду вузла, який він збирається відвідати. По-друге, незрозуміло, які ваші приклади хороші чи погані? Як вони пов’язані з моїм запитанням?
Валь
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.