Сейчас много говорят об отделении алгоритмов от классов. Но одна вещь остается в стороне и не объясняется.
Они используют посетителя вот так
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);
}
Вместо прямого вызова visit (element) Visitor просит элемент вызвать его метод посещения. Это противоречит заявленному представлению о классовой неосведомленности о посетителях.
PS1 Пожалуйста, объясните своими словами или укажите точное объяснение. Потому что два ответа, которые я получил, относятся к чему-то общему и неопределенному.
PS2 Мое предположение: поскольку getLeft()
возвращает базовый Expression
, вызов visit(getLeft())
приведет к visit(Expression)
, тогда как getLeft()
вызов visit(this)
приведет к другому, более подходящему вызову посещения. Итак, accept()
выполняет преобразование типа (также известное как приведение).
Соответствие шаблонов PS3 Scala = шаблон посетителя на Steroid показывает, насколько проще шаблон посетителя без метода accept. Википедия добавляет к этому утверждению : путем ссылки на статью, показывающую, что « accept()
методы не нужны, когда рефлексия доступна; вводит термин« обход »для техники».
Ответы:
Шаблоны
visit
/accept
конструкции посетителя являются неизбежным злом из-за семантики C-подобных языков (C #, Java и т. Д.). Цель шаблона посетителя - использовать двойную отправку для маршрутизации вашего вызова, как и следовало ожидать от чтения кода.Обычно, когда используется шаблон посетителя, задействуется иерархия объектов, в которой все узлы являются производными от базового
Node
типа, далее именуемогоNode
. Инстинктивно мы бы написали это так:В этом и заключается проблема. Если бы наш
MyVisitor
класс был определен следующим образом:Если во время выполнения, независимо от фактического типа
root
, наш вызов перейдет в перегрузкуvisit(Node node)
. Это будет верно для всех переменных, объявленных типаNode
. Почему это? Поскольку Java и другие языки, подобные C, учитывают только статический тип или тип, в котором объявлена переменная, параметра при принятии решения, какую перегрузку вызывать. Java не делает лишнего шага, чтобы спрашивать при каждом вызове метода во время выполнения: «Хорошо, что это за динамический типroot
? О, понятно. Это аTrainNode
. Посмотрим, есть ли какой-нибудь метод, вMyVisitor
котором принимает параметр типаTrainNode
... ". Компилятор во время компиляции определяет, какой метод будет вызван (если бы Java действительно проверила динамические типы аргументов, производительность была бы довольно ужасной).Java действительно дает нам один инструмент для учета рабочего (т.е. динамического) типа объекта при вызове метода - диспетчеризация виртуального метода . Когда мы вызываем виртуальный метод, вызов фактически переходит к таблице в памяти, которая состоит из указателей на функции. У каждого типа есть таблица. Если конкретный метод переопределяется классом, запись в таблице функций этого класса будет содержать адрес замещенной функции. Если класс не переопределяет метод, он будет содержать указатель на реализацию базового класса. Это по-прежнему влечет за собой накладные расходы (каждый вызов метода будет в основном разыменовывать два указателя: один указывает на таблицу функций типа, а другой - на саму функцию), но это все же быстрее, чем необходимость проверки типов параметров.
Цель шаблона посетителя - выполнить двойную отправку - учитывается не только тип цели вызова (
MyVisitor
через виртуальные методы), но также тип параметра (какой типNode
мы смотрим)? Шаблон Visitor позволяет нам делать это с помощью комбинацииvisit
/accept
.Изменив нашу строку на эту:
Мы можем получить то, что хотим: с помощью диспетчеризации виртуального метода мы вводим правильный вызов accept (), реализованный в подклассе - в нашем примере с помощью
TrainElement
мы вводимTrainElement
реализациюaccept()
:Что ноу компилятор на данный момент, внутри рамки
TrainNode
-хaccept
? Он знает, что статический типthis
- этоTrainNode
. Это важный дополнительный фрагмент информации, о которой компилятор не знал в области видимости нашего вызывающего объекта: все, о чем он зналroot
, это то, что это был файлNode
. Теперь компилятор знает, чтоthis
(root
) - это не простоNode
, а на самом делеTrainNode
. Следовательно, одна строка внутриaccept()
:v.visit(this)
означает нечто совершенно иное. Теперь компилятор будет искать перегрузкуvisit()
, требующуюTrainNode
. Если он не может его найти, он скомпилирует вызов перегрузки, которая требуетNode
. Если ни один из них не существует, вы получите ошибку компиляции (если у вас нет перегрузки, которая требуетobject
). Таким образом, Execution войдет в то, что мы планировалиMyVisitor
с самого начала : реализацияvisit(TrainNode e)
. Никаких слепков и, самое главное, рефлексии не требовалось. Таким образом, накладные расходы этого механизма довольно низкие: он состоит только из ссылок на указатели и ничего больше.Вы правы в своем вопросе - мы можем использовать приведение и добиться правильного поведения. Однако часто мы даже не знаем, что такое Node. Возьмем следующую иерархию:
И мы писали простой компилятор, который анализирует исходный файл и создает иерархию объектов, соответствующую приведенной выше спецификации. Если бы мы писали интерпретатор для иерархии, реализованной как Посетитель:
Кастинг не получили бы нас очень далеко, так как мы не знаем , типов
left
илиright
вvisit()
методах. Наш синтаксический анализатор, скорее всего, также просто вернет объект типа,Node
который также указывает на корень иерархии, поэтому мы также не можем безопасно преобразовать его. Итак, наш простой интерпретатор может выглядеть так:Шаблон посетителя позволяет нам делать что-то очень мощное: учитывая иерархию объектов, он позволяет нам создавать модульные операции, которые работают над иерархией, не требуя размещения кода в самом классе иерархии. Шаблон посетителя широко используется, например, при построении компилятора. С учетом синтаксического дерева конкретной программы написано множество посетителей, которые работают с этим деревом: проверка типов, оптимизация, выпуск машинного кода обычно реализуются как разные посетители. В случае посетителя оптимизации он может даже вывести новое синтаксическое дерево с учетом входного дерева.
Конечно, у него есть свои недостатки: если мы добавляем новый тип в иерархию, нам нужно также добавить
visit()
метод для этого нового типа вIVisitor
интерфейс и создать заглушки (или полные) реализации для всех наших посетителей. Нам также необходимо добавитьaccept()
метод по причинам, описанным выше. Если производительность не так много для вас значит, есть решения для написания посетителей без необходимостиaccept()
, но они обычно включают отражение и, следовательно, могут повлечь за собой довольно большие накладные расходы.источник
accept()
Метод становится необходимым, когда это предупреждение нарушается в Visitor.Конечно, это было бы глупо, если бы это был единственный способ реализации Accept.
Но это не так.
Например, посетители действительно очень полезны при работе с иерархиями, и в этом случае реализация нетерминального узла может быть примерно такой
Ты видишь? То, что вы называете глупым, - это решение для обхода иерархий.
Вот гораздо более длинная и подробная статья, которая заставила меня понять посетителя .
Изменить: уточнить:
Visit
метод посетителя содержит логику, применяемую к узлу. Метод узлаAccept
содержит логику перехода к соседним узлам. Случай, когда вы выполняете только двойную отправку, является особым случаем, когда просто нет соседних узлов для перехода.источник
Назначение шаблона посетителя - гарантировать, что объекты знают, когда посетитель закончил с ними и ушел, чтобы классы могли впоследствии выполнить любую необходимую очистку. Это также позволяет классам "временно" открывать свои внутренние компоненты как параметры 'ref' и знать, что внутренние компоненты больше не будут отображаться после ухода посетителя. В случаях, когда очистка не требуется, шаблон посетителя не очень полезен. Классы, которые не выполняют ни одну из этих вещей, могут не получить выгоду от шаблона посетителя, но код, написанный для использования шаблона посетителя, будет использоваться с будущими классами, которые могут потребовать очистки после доступа.
Например, предположим, что у кого-то есть структура данных, содержащая много строк, которые должны обновляться атомарно, но класс, содержащий структуру данных, не знает точно, какие типы атомарных обновлений следует выполнять (например, если один поток хочет заменить все вхождения " X ", в то время как другой поток хочет заменить любую последовательность цифр на последовательность, которая численно на единицу выше, операции обоих потоков должны завершиться успешно; если каждый поток просто считал строку, выполнил свои обновления и записал ее обратно, второй поток для обратной записи его строка перезапишет первую). Один из способов добиться этого - заставить каждый поток получить блокировку, выполнить свою операцию и снять блокировку. К сожалению, если блокировки открыты таким образом,
Паттерн Посетитель предлагает (по крайней мере) три подхода, позволяющих избежать этой проблемы:
Без шаблона посетителя выполнение атомарных обновлений потребовало бы раскрытия блокировок и риска отказа, если вызывающее программное обеспечение не соблюдает строгий протокол блокировки / разблокировки. С шаблоном Visitor атомарные обновления могут выполняться относительно безопасно.
источник
Все классы, требующие модификации, должны реализовывать метод accept. Клиенты вызывают этот метод accept, чтобы выполнить какое-то новое действие над этим семейством классов, тем самым расширяя их функциональность. Клиенты могут использовать этот единственный метод accept для выполнения широкого спектра новых действий, передавая другой класс посетителей для каждого конкретного действия. Класс посетителя содержит несколько переопределенных методов посещения, определяющих, как выполнить одно и то же конкретное действие для каждого класса в семействе. Этим методам посещения передается экземпляр, над которым можно работать.
Посетители полезны, если вы часто добавляете, изменяете или удаляете функциональные возможности в стабильном семействе классов, потому что каждый элемент функциональности определяется отдельно в каждом классе посетителя, и сами классы не нуждаются в изменении. Если семейство классов нестабильно, то шаблон посетителя может быть менее полезен, потому что многие посетители нуждаются в изменении каждый раз, когда класс добавляется или удаляется.
источник
Хороший пример в исходном коде компиляции:
Клиенты могут реализовать
JavaBuilder
,RubyBuilder
,XMLValidator
и т.д. , и осуществление сбора и посещения всех исходных файлов в проекте не требуется изменений.Это будет плохой шаблон, если у вас есть отдельные классы для каждого типа исходного файла:
Все сводится к контексту и тем частям системы, которые вы хотите расширить.
источник