访问者模式对动态类型语言有用吗?


Is the Visitor pattern useful for dynamically typed languages?

访问者模式允许在不扩展对象类的情况下写入对对象的操作。 当然。但是,为什么不直接编写一个全局函数或静态类,从外部操作我的对象集合呢?基本上,在像 java 这样的语言中,出于技术原因需要accept()方法;但是在一种语言中,我可以在没有accept()方法的情况下实现相同的设计,访问者模式是否变得微不足道?

说明:在访问者模式中,可访问类(实体)有一个方法.accept()其工作是调用访问者的.visit()方法。我可以看到java示例的逻辑:访问者为其支持的每种可访问类型定义不同的.visit(n)方法n并且必须在运行时使用.accept()技巧在它们之间进行选择。但是像python或php这样的语言具有动态类型,并且没有方法重载。如果我是访问者,我可以调用实体方法(例如,.serialize()),而无需知道实体的类型,甚至不知道方法的完整签名。(这就是"双重调度"问题,对吧?

我知道接受方法可以将受保护的数据传递给访问者,但有什么意义呢?如果数据公开给访问者类,则它实际上是类接口的一部分,因为它的详细信息在类之外很重要。无论如何,公开私人数据从来没有让我成为访问者模式的重点。

所以似乎在 python、ruby 或 php 中,我可以在访问的对象中实现一个类似访问者的类,而无需接受方法(并且没有反射),对吧?如果我可以使用一系列异构对象,并在没有"访问"类任何合作的情况下调用它们的公共方法,这是否仍然值得称为"访客模式"?我是否缺少模式的本质,或者它只是归结为"编写一个新类,从外部操作您的对象以执行操作"?

附言。我已经查看了有关SO和其他地方的大量讨论,但找不到任何解决此问题的内容。欢迎指点。

访问者特别有用的地方是访问者需要打开 Visitees 类型的地方,无论出于何种原因,您都不想将该知识编码到 Visitees 中(想想插件架构)。考虑以下 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

(请注意,我们可以使用基类/mixin压缩访问者逻辑)。

儗:

非访客风格

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

在第二个示例中,水果不需要对"访问者"的任何特殊支持,并且"访问者"处理给定类型的逻辑缺失。

相比之下,在Java或C++中,第二个示例实际上是不可能的,访问方法(在访问者中)可以使用一个名称来引用进程方法的所有版本;编译器将选择适用于所传递类型的版本;访问者可以轻松地为访问者类型的根类提供默认实现。在访问者中还需要有一个访问方法,因为方法变体(例如 process(Banana b) vs process(Apple a) ) 在编译时在为访问者的visit方法生成的代码中选择。

因此,在像Python或Ruby这样的语言中,没有参数类型的调度(或者更确切地说,程序员必须自己实现它),不需要访问者模式。或者,有人可能会说,无需通过访问者方法调度,访客模式会更好地实现。

一般来说,在像Python,Ruby或Smalltalk这样的动态语言中,最好让"visitee"类携带所有需要的信息(这里,动词适用),并在必要时提供钩子来支持"访问者",例如命令或策略模式,或使用此处显示的非访问者模式。

结论

非访客是实现类型切换逻辑的干净方法,尽管显式类型切换通常是一种代码异味。请记住,Java 和C++方式也是访问者中的显式切换;这些语言中模式的优雅之处在于它避免了访问者中显式切换逻辑,这在具有非类型变量的动态语言中是不可能的。因此,顶部的访问者模式对动态语言不利,因为它再现了静态语言中的访问者模式试图避免的罪恶。

使用模式的问题在于,与其盲目地复制UML图,不如了解它们要实现的目标,以及它们如何通过具体考虑的语言机制来实现这些目标。在这种情况下,实现相同优点的模式看起来不同,并且具有不同的调用模式。这样做可以让您使它们适应不同的语言,也可以适应同一语言中的不同具体情况。

更新:这是一篇关于实现此模式的 ruby 文章:http://blog.rubybestpractices.com/posts/aaronp/001_double_dispatch_dance.html

双重调度对我来说似乎是被迫的;据我所知,你可以取消它。

这个答案是在对 PHP 等一无所知的情况下做出的,但访问者通常需要在实体上调用不止一个方法(您提到了"序列化")。 当在具体的 Visitor 上调用 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,如果不是。

关于处理枚举的访客,我认为您正在这样限制自己。 这并不是说合作类(抽象的访问者枚举器)不能参与。

例如,请注意此访问者不知道枚举的顺序:

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 子类以完全分离它们。 希望您能看到将枚举分开的力量。

我认为您正在互换使用访客模式和双重调度。当你说,

如果我可以使用一系列异构对象,并在没有"访问"类任何合作的情况下调用它们的公共方法,这是否仍然值得称为"访客模式"?

编写一个新类,从外部操作您的对象以执行操作"?

您正在定义什么是双重调度。当然,访客模式是通过双重调度实现的。但模式本身还有更多的东西。

  • 每个访问者都是一组元素(实体)上的算法,可以在不更改现有代码的情况下插入新访问者。开/闭原则。
  • 当频繁添加新元素时,最好避免访客模式

也许,这取决于语言。

访客模式解决了不具有多重调度功能的语言中的双重和多层次结构问题。以Ruby,Lisp和Python为例。它们都是动态类型的语言,但只有CLOS-Lisp在标准中实现了多重调度。这也被称为多方法,Python和Ruby显然可以通过使用扩展来实现它。

我喜欢维基百科上的这个奇怪的评论,指出:

Lisp 的对象系统 [CLOS] 及其多重调度并没有取代 Visitor 模式, 但只是提供了更简洁的实现,其中模式几乎 消失。

在其他语言中,即使是静态类型的语言,您也必须解决缺少多方法的问题。访客模式就是这样一种方式。

访问者模式对我来说意味着根据对象的类型为对象添加新功能。显然,使用 if/else 梯子来执行特定于类型的操作是不好的(我想对此:(进行解释)。在python中,我能够做到这一点,没有整个双重调度戏剧,通过将猴子补丁(另一个坏主意)某些函数作为类方法。

我在这里问过这个。

在下面的示例中,假设有一个基类ASTNode和它下面的大型类层次结构(ASTVarASTModuleASTIfASTConst等)。这些类只有其特定的数据属性和简单的方法。

然后,假设类代码被锁定(或者功能可能与数据分离)。现在,我有动态分配给类的方法。请注意,在下面的示例中,迭代/递归方法调用名称 (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中的访问者模式)吗?

这不就是访客模式的目标吗?代码锁定类型的扩展?当然,在动态类型的python中不需要使用双重调度(VisitorObject.visit(ConcreteObject)ConcreteObject.Accept(VisitorObject)调用)。也许有人会将其形式化为动态类型语言,并且我们将手头有一个新的模式,或者没有。 毕竟,模式是发现的,而不是发明的(我不记得我在哪里读到的)。

访客模式做两件事:

  • 允许临时多态性(相同的功能但做不同的事情到不同的"类型")。
  • 允许在不更改数据提供程序的情况下添加新的使用算法。

您可以在没有访问者或运行时类型信息的动态语言中执行第二项操作。但第一个需要一些明确的机制,或者像 Visitor 这样的设计模式。