Полезен ли шаблон посетителя для языков с динамической типизацией?

Шаблон Посетитель позволяет записывать операции над объектами без расширения класса объекта. Конечно. Но почему бы просто не написать глобальную функцию или статический класс, который манипулирует моей коллекцией объектов извне? По сути, в таком языке, как java, метод accept() необходим по техническим причинам; но на языке, где я могу реализовать тот же дизайн без метода accept(), становится ли шаблон посетителя тривиальным?

Объяснение: В шаблоне Посетитель доступные для посещения классы (сущности) имеют метод .accept(), задачей которого является вызов метода .visit() посетителя для самих себя. Я вижу логику примеров Java: посетитель определяет разные .visit(n) методы для каждого доступного для посещения типа n, который он поддерживает, и для выбора среди них во время выполнения необходимо использовать прием .accept(). Но такие языки, как python или php, имеют динамическую типизацию и не перегружают методы. Если я посетитель, я могу вызвать метод сущности (например, .serialize()), не зная ни типа сущности, ни даже полной подписи метода. (Это проблема «двойной отправки», верно?)

Я знаю, что метод accept может передать посетителю защищенные данные, но какой в ​​этом смысл? Если данные доступны классам-посетителям, они фактически являются частью интерфейса класса, поскольку его детали имеют значение вне класса. Во всяком случае, раскрытие личных данных никогда не казалось мне целью шаблона поведения посетителей.

Итак, кажется, что в python, ruby ​​или php я могу реализовать класс, подобный посетителю, без метода accept в посещаемом объекте (и без отражения), верно? Если я могу работать с семейством разнородных объектов и вызывать их общедоступные методы без какого-либо сотрудничества с «посещенным» классом, заслуживает ли это того, чтобы называться «шаблоном посетителя»? Есть ли что-то в сути паттерна, чего мне не хватает, или он просто сводится к тому, чтобы «написать новый класс, который манипулирует вашими объектами извне для выполнения операции»?

PS. Я просмотрел множество дискуссий по SO и в других местах, но не смог найти ничего, что касалось бы этого вопроса. Указатели приветствуются.


person alexis    schedule 22.06.2012    source источник
comment
Внимательно прочитав ваш вопрос, я вижу, что мой ответ — многословный способ согласиться с вами.   -  person Marcin    schedule 17.04.2015
comment
Ха, спасибо :-) Хорошо, что этот вопрос снова открыт... Спустя более двух лет после того, как я его задал, я все еще не уверен, в чем заключается оставшаяся сущность шаблона Посетитель, после того как он почти исчез (как выразилась википедия это) на питоне и подобных языках...   -  person alexis    schedule 19.04.2015
comment
Я думаю, что оставшаяся суть — это повторяющийся шаблон стратегии. Но с другой стороны, Visitor всегда был таким, с трюками с двойной отправкой для обработки разных типов.   -  person Marcin    schedule 22.04.2015


Ответы (6)


Этот ответ сделан с незнанием PHP и т. д., но посетителю обычно нужно вызывать более одного метода (вы упомянули «сериализацию») для объектов. Когда метод Visit() вызывается для конкретного посетителя, посетитель может выполнять совершенно другой код для каждого подтипа сущности. Я не вижу, чем это отличается от языка с динамическими типами (хотя мне бы хотелось получить обратную связь).

Еще одним приятным преимуществом Visitor является то, что он обеспечивает четкое разделение кода, который запускается для каждой сущности, от кода, который перечисляет сущности. Это спасло меня от серьезного дублирования кода по крайней мере в одном крупном проекте.

Кроме того, я использовал Visitor в языках, в которых не было перегрузки методов. Вы просто заменяете Visit(TypeN n) на VisitN(TypeN n).


Следите за комментариями.

Это псевдокод посетителя, и я не знаю, как бы я это сделал без сотрудничества с посещаемым объектом (по крайней мере, без блока переключателя):

abstract class ScriptCommand
{
   void Accept(Visitor v);
}

abstract class MoveFileCommand
{
   string TargetFile;
   string DestinationLocation;

   void Accept(Visitor v)
   {
      v.VisitMoveFileCmd(this);  // this line is important because it eliminates the switch on object type
   }
}

abstract class DeleteFileCommand
{
   string TargetFile;

   void Accept(Visitor v)
   {
      v.VisitDeleteFileCmd(this); // this line is important because it eliminates the switch on object type

   }
}

// etc, many more commands

abstract class CommandVisitor
{
   void VisitMoveFileCmd(MoveFileCommand cmd);
   void VisitDeleteFileCmd(DeleteFileCommand cmd);
   // etc
}

// concrete implementation

class PersistCommandVisitor() inherits CommandVisitor
{
   void VisitMoveFileCmd(MoveFileCommand cmd)
   {
      // save the MoveFileCommand instance to a file stream or xml doc
      // this code is type-specific because each cmd subtype has vastly
      // different properties
   }

   void VisitDeleteFileCmd(DeleteFileCommand cmd)
   { 
      // save the DeleteFileCommand instance to a file stream or xml doc
      // this code is type-specific because each cmd subtype has vastly
      // different properties
   }

}

Инфраструктура посетителя позволяет обрабатывать широкий спектр подтипов команд без выбора регистра, swithc, если еще.

Что касается посетителя, занимающегося перечислением, я думаю, что вы ограничиваете себя таким образом. Это не означает, что взаимодействующий класс (абстрактный VisitorEnumerator) не может быть задействован.

Например, обратите внимание, что этот посетитель не знает порядка перечисления:

class FindTextCommandVisitor() inherits CommandVisitor
{
   string TextToFind;
   boolean TextFound = false;

   void VisitMoveFileCmd(MoveFileCommand cmd)
   {
      if (cmd.TargetFile.Contains(TextToFind) Or cmd.DestinationLocation.Contains(TextToFind))
         TextFound = true;
   }


   void VisitDeleteFileCmd(DeleteFileCommand cmd)
   { 
      // search DeleteFileCommand's properties
   }

}

И это позволяет повторно использовать его следующим образом:

ScriptCommand FindTextFromTop(string txt)
{
   FindTextCommandVisitor v = new FindTextCommandVisitor();
   v.TextToFind = txt;
   for (int cmdNdx = 0; cmdNdx < CommandList.Length; cmdNdx++)
   {
      CommandList[cmdNdx].Accept(v);
      if (v.TextFound)
         return CommandList[cmdNdx];  // return the first item matching
   }
}

и перечислить наоборот с тем же посетителем:

ScriptCommand FindTextFromBottom(string txt)
{
   FindTextCommandVisitor v = new FindTextCommandVisitor();
   v.TextToFind = txt;
   for (int cmdNdx = CommandList.Length-1; cmdNdx >= 0; cmdNdx--)
   {
      CommandList[cmdNdx].Accept(v);
      if (v.TextFound)
         return CommandList[cmdNdx];  // return the first item matching
   }
}

В реальном коде я бы создал базовый класс для перечислителя, а затем создал бы его подкласс для обработки различных сценариев перечисления, передав конкретный подкласс Visitor, чтобы полностью разделить их. Надеюсь, вы видите силу сохранения перечисления отдельно.

person tcarvin    schedule 22.06.2012
comment
Суть моего вопроса в том, что вы можете написать visitN() для каждого типа и можете напрямую вызывать различные методы объекта, но ни одно из этих действий не требует взаимодействия с посещаемым объектом. Чем шаблон посетителя отличается от любой глобальной функции, выполняющей операцию над объектами? Что касается перечисления: многие считают лучшей практикой для посетителя обрабатывать перечисление. - person alexis; 22.06.2012
comment
Я понимаю. Таким образом, посетитель должен предоставить меню методов, и посещаемый объект выбирает из них (явно, в этом случае), вместо того, чтобы посетитель предоставлял карту типа-метода. И все посетители должны называть свои методы одинаково, верно? - person alexis; 22.06.2012
comment
Кстати, я нашел ваш выбор примера неудачным: посетитель обычно представляет собой операцию над классом объектов, но ваши объекты сами являются операциями. Я предполагаю, что они представляют элементы графического интерфейса, а не действия, но я нашел этот вариант использования излишне запутанным. Просто говорю. - person alexis; 22.06.2012
comment
Обычно у вас будет базовый класс или интерфейс Visitor и вы наследуете методы, что обеспечивает их одинаковое имя в конкретных подклассах Visitor. - person tcarvin; 22.06.2012
comment
Что касается моего выбора примера, весь смысл концепции шаблонов заключается в том, что они работают в разных областях. Посетитель одинаково действителен при использовании с элементами графического интерфейса пользователя, как и с интерпретатором сценариев. Хотя, если вам от этого станет легче, интерпретатор сценариев, который я смоделировал в примере из является графическим, и такие операции, как FindText, используются для поиска и выделения графического представления команды. Извините, но это было запутанно. - person tcarvin; 22.06.2012
comment
Хорошо спасибо. Но я все еще задаюсь вопросом: если набор управляемых классов не собирается меняться (важное предварительное условие для шаблона посетителя, как все говорят), действительно ли сопоставление типа с методом хуже, чем распространение всей этой сложности? В чем большая выгода? - person alexis; 22.06.2012
comment
давайте продолжим это обсуждение в чате - person tcarvin; 22.06.2012
comment
ТАК ныл, что я позволил ему перейти в чат (которым я никогда не пользовался). Дальнейшие комментарии по этой ссылке - person tcarvin; 22.06.2012
comment
Проблема с этим ответом в том, что он на самом деле не касается динамических языков. Динамические языки (с нетипизированными переменными) не имеют возможности (как правило) выбирать между методами на основе типа аргументов методов. Это означает, что для воспроизведения традиционной отправки в стиле посетителя шаблона посетителя посетитель должен предоставить методы с другими именами; это воспроизводит запах кода, которого посетитель должен избегать. - person Marcin; 17.04.2015
comment
Также прочитайте связанное обсуждение в чате, так как там обсуждается ваша точка зрения. Как обсуждалось более подробно, посетитель имеет более чем одно преимущество, и он по-прежнему имеет ценность в языке с динамической типизацией ... просто не так много, как в языке со статической типизацией. Ваш пример ниже немного надуман, потому что вы делаете одно и то же с каждым из методов процесса. В приведенном выше примере скрипт-команды логика каждого из них сильно различается, и вам потребуется, чтобы ваш NonVisitorVisitor затем использовал переключатель или другой механизм для демультиплексирования параметра fruit. - person tcarvin; 17.04.2015

Место, где посетитель особенно полезен, — это то, где посетителю нужно включить тип посетителей, и по какой-то причине вы не хотите кодировать это знание в посетителях (подумайте о архитектуре плагинов). Рассмотрим следующий код Python:

Стиль посетителя

class Banana(object):
      def visit(self, visitor):
          visitor.process_banana(self) 

class Apple(object):
      def visit(self, visitor):
          visitor.process_apple(self) 

class VisitorExample(object):
      def process_banana(self, banana):
          print "Mashing banana: ", banana

      def process_banana(self, apple):
          print "Crunching apple: ", apple

(Обратите внимание, что мы можем сжать логику посетителя с помощью базового класса/примеси).

Сравнить с:

Стиль без посетителей

class NonVisitorVisitor(object):
      def process(self, fruit):
          verb = {Banana: "Mashing banana: ", 
                  Apple: "Crunching apple: "}[type(fruit)]
          print verb, fruit

Во втором примере фрукты не нуждаются в специальной поддержке «посетителя», а «посетитель» обрабатывает отсутствие логики для данного типа.

Напротив, в Java или C++ второй пример на самом деле невозможен, и метод посещения (в посетителях) может использовать одно имя для ссылки на все версии метода процесса; компилятор выберет версию, которая относится к передаваемому типу; и посетитель может легко предоставить реализацию по умолчанию для корневого класса для типа посетителей. Также необходимо иметь метод посещения в посетителях, потому что вариант метода (например, process(Banana b) против process(Apple a)) выбирается во время компиляции в коде, сгенерированном для метода посетителя visit.

Соответственно, в таких языках, как Python или Ruby, где нет диспетчеризации по типам параметров (точнее, программисту приходится реализовывать ее самому), шаблон посетитель не нужен. В качестве альтернативы можно сказать, что шаблон посетителя лучше реализован без диспетчеризации через методы посетителя.

В целом, в динамических языках, таких как Python, Ruby или Smalltalk, лучше, чтобы классы «посетителей» содержали всю необходимую информацию (здесь применим глагол) и, при необходимости, предоставляли крючки для поддержки «посетителя», например в качестве командных или стратегических паттернов или используйте паттерн «Не посетитель», показанный здесь.

Вывод

Non-Visitor — это чистый способ реализации логики переключения типов, несмотря на то, что явное переключение типов обычно является запахом кода. Помните, что в Java и C++ для этого также используется явное переключение в посетителе; элегантность шаблона в этих языках заключается в том, что он позволяет избежать явной логики переключения в посетителях, что невозможно в динамических языках с нетипизированными переменными. Соответственно, шаблон «Посетитель» вверху плох для динамических языков, потому что воспроизводит грех, которого шаблон «Посетитель» пытается избежать в статических языках.

Суть использования шаблонов заключается в том, что вместо того, чтобы рабски воспроизводить диаграммы UML, вы должны понимать, чего они пытаются достичь, и как они достигают этих целей с помощью конкретно рассматриваемого языкового механизма. В этом случае схема достижения одних и тех же достоинств выглядит по-другому и имеет другую схему вызовов. Это позволит вам адаптировать их к разным языкам, а также к различным конкретным ситуациям в пределах одного языка.

Обновление: вот рубиновая статья о реализации этого шаблона: http://blog.rubybestpractices.com/posts/aaronp/001_double_dispatch_dance.html

Двойная отправка кажется мне довольно вынужденной; Вы могли бы просто покончить с этим, насколько я могу судить.

person Marcin    schedule 17.04.2015
comment
Ясно... Итак, класс Example реализует набор базовых действий (возможно, стандартные компоненты отображения, такие как таблицы или списки), а затем говорит посетителям отображать себя, и они используют для этого методы посетителя? И другой посетитель может предоставить другие реализации, например. PDF против HTML... я на правильном пути? Прошли годы с тех пор, как я задал вопрос, но я до сих пор не понимаю, как правильно его использовать... - person alexis; 18.04.2015
comment
Я имел в виду, что один посетитель может отображать себя в виде списка, другой — в виде таблицы и т. д.; посетитель знает, как нарисовать таблицу, но не знает, когда это уместно. Но я больше не уверен, соответствует ли это тому, что вы имели в виду. - person alexis; 18.04.2015
comment
@alexis Я понимаю, что ты имеешь в виду. То, что вы описываете, я думаю, не совсем шаблон посетителя, а форма шаблона стратегии. На самом деле, я думаю, что это проблема разговоров о шаблонах в том виде, в каком они существуют сейчас - они действительно фокусируются на том, сколько стоят шаблоны диаграмм UML, совершенно конкретным образом, не сосредотачиваясь на лучших практиках, которые они воплощают, или определяя, как шаблоны могут или не могут компенсировать или относиться к конкретным языковым недостаткам/особенностям. Шаблон стратегии посетителя имеет смысл; но опять же, это еще одна слабость разговора о шаблонах: он не обсуждает, как соотносятся шаблоны. - person Marcin; 18.04.2015
comment
Спасибо! Тогда я освежу в памяти схему стратегии. Возвращаясь к этому вопросу, было полезно: описанный мной сценарий соответствует приложению, над которым я работаю (хотя и с одним выходным форматом). - person alexis; 19.04.2015
comment
Теперь мне показалось, что в реальном шаблоне посетителя посетитель знает, какой метод он хочет вызвать, но посетители знают, какие данные они хотят предоставить... но это не то, что показывает ваш пример. В вашем примере посетитель предоставляет методы, и визит означает, что посетители вызывают мои методы. Если посетители знают, какой метод посетителя они должны вызвать, чем это отличается от шаблона стратегии? - person alexis; 19.04.2015
comment
@alexis В Java/C++ тип посетителя выбирает правильный метод для вызова; в моем примере посетитель должен указать свой тип при вызове. Разница со стратегией в том, что посетитель должен вызывать только метод, соответствующий его собственному типу. Моя проблема с шаблонами GOF заключается в том, что они не обязательно суперотличны и смешивают кучу разных соображений. - person Marcin; 19.04.2015

Я думаю, что вы используете шаблон посетителя и двойную отправку взаимозаменяемо. Когда ты говоришь,

Если я могу работать с семейством разнородных объектов и вызывать их общедоступные методы без какого-либо сотрудничества с "посещенным" классом, заслуживает ли это того, чтобы называться "шаблоном посетителя"?

и

написать новый класс, который манипулирует вашими объектами извне для выполнения операции"?

вы определяете, что такое двойная отправка. Конечно, паттерн Посетитель реализуется двойной диспетчеризацией. Но есть еще кое-что в самом узоре.

  • Каждый посетитель представляет собой алгоритм для группы элементов (сущностей), и новые посетители могут быть подключены без изменения существующего кода. Принцип «открыто/закрыто».
  • Когда новые элементы добавляются часто, лучше избегать шаблона «Посетитель».
person user1168577    schedule 22.06.2012

Возможно, это зависит от языка.

Шаблон посетителя решает проблемы двойной и множественной иерархии в языках, не поддерживающих множественную отправку. Возьмите Ruby, Lisp и Python. Все они являются языками с динамической типизацией, но только CLOS-Lisp реализует в стандарте множественную диспетчеризацию. Это также называется мультиметодами, и Python и Ruby, по-видимому, могут реализовать его с помощью расширений.

Мне нравится любопытный комментарий к википедии, в котором говорится, что:

Объектная система Лиспа [CLOS] с ее множественной диспетчеризацией не заменяет шаблон Посетитель, а просто обеспечивает его более сжатую реализацию, в которой шаблон практически исчезает.

В других языках, даже в статически типизированных, приходится обходиться без мультиметодов. Шаблон Посетитель является одним из таких способов.

person Joao Tavora    schedule 22.06.2012
comment
На самом деле я не думаю, что посетитель осмысленно решает проблему двойной отправки в языках, которые не могут выполнять какую-либо отправку для типов параметров; вот почему он работает в C++, Java и C#, но не в Python или Ruby. - person Marcin; 18.04.2015

Шаблон посетителя для меня означал добавление новых функций к объектам в зависимости от их типа. По-видимому, иметь лестницы if/else для выполнения операций, специфичных для типа, плохо (мне нужно объяснение этого :( ). В python я смог сделать это, без всей драмы двойной отправки, с помощью monkeypatching (еще одна плохая идея) определенные функции как методы класса.

Я спросил об этом здесь.

В приведенном ниже примере предположим, что есть базовый класс ASTNode и большая иерархия классов под ним (ASTVar, ASTModule, ASTIf, ASTConst и т. д.). Эти классы имеют только свои определенные атрибуты данных и тривиальные методы.

Затем предположим, что код класса заблокирован (или, возможно, функциональность отделена от данных). Теперь у меня есть методы, которые динамически назначаются классам. Обратите внимание, что в приведенном ниже примере имя вызова метода итерации/рекурсии (stringify) отличается от имени функции (nodeType_stringify).

def ASTNode__stringify(self):
    text = str(self)
    for child in self.children:
            text += ", { " + child.stringify() + " }"
    return text

def ASTConst__stringify(self):
    text = str(self)
    for child in self.children:
            text += ", [ " + child.stringify() + " ]"
    return text

def ASTIf__stringify(self):
    text = str(self)
    text += "__cond( " + self.op1.stringify() + ")"
    text += "__then { " + self.op2.stringify() + "}"
    text += "__else {" + self.op3.stringify() + "}"
    return text

Я могу расширить классы (возможно, один раз во время инициализации модуля) функциональностью, когда захочу (плохая идея?).

# mainModule1.py
def extend_types():
    # ASTNode and all derived class get this method
    ASTNode.stringify = ASTNode__stringify
    ASTConst.stringify = ASTConst__stringify
    ASTIf.stringify = ASTIf__stringify

Теперь вызов my_root_node.stringify() будет соответствующим образом вызывать правильные дочерние методы (рекурсивно), без явной проверки типа.

Разве этот метод не похож на добавление методов к прототипам Javascript (шаблон посетителя в JS< /а>).

Разве не в этом заключалась цель Visitor Pattern? Расширение типов с кодовым замком? Конечно, необходимость использовать двойную отправку (VisitorObject.visit(ConcreteObject) вызывается ConcreteObject.Accept(VisitorObject)) не потребуется в python, который имеет динамическую типизацию. Возможно, кто-то формализует это для динамически типизированных языков, и будет у нас на руках новый паттерн, или нет. В конце концов, закономерности открываются, а не изобретаются (не помню, где я это читал).

person Sonal Pinto    schedule 01.10.2016

Шаблон посетителя делает 2 вещи:

  • Допускает специальный полиморфизм (одна и та же функция, но разные действия для разных «типов»).
  • Позволяет добавить новый алгоритм потребления без смены поставщика данных.

Вы можете сделать второе на динамических языках без информации о посетителе или времени выполнения. Но сначала требуется какой-то явный механизм или шаблон проектирования, такой как Посетитель.

person przemo_li    schedule 23.11.2017