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

时间:2012-06-22 10:43:28

标签: php python ruby design-patterns visitor

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

说明:在访问者模式中,可访问的类(实体)有一个方法.accept(),其作用是调用访问者的.visit()方法。我可以看到的Java示例的逻辑:访问者定义了每个的Visitable类型.visit(n),它支持一个不同的n的方法,和.accept()特技必须使用在运行当中选择。但是像python或php这样的语言有动态类型,没有方法重载。如果我是访问者,我可以在不知道实体类型甚至方法的完整签名的情况下调用实体方法(例如,.serialize())。 (这是“双重调度”问题,对吗?)

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

所以看起来在python,ruby或php中我可以实现一个类似访问者的类,在访问对象中没有accept方法(并且没有反射),对吧?如果我可以使用一系列异构对象并在没有“访问”类的任何合作的情况下调用他们的公共方法,那么这仍然应该被称为“访问者模式”吗?是否有东西,我很想念,还是只是归结为“写的是从外面操纵你的对象一个新类来进行操作”模式的本质是什么?

PS。我已经看过很多关于SO和其他地方的讨论,但找不到任何解决这个问题的内容。指针欢迎。

6 个答案:

答案 0 :(得分:2)

访问者特别有用的地方是访问者需要打开访问者类型的地方,无论出于何种原因,您都不想将这些知识编码到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压缩visitee逻辑。)

与:比较:

非访客风格

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

在第二个例子中,水果不需要为访客"和访客"以及#34;访客"处理给定类型的逻辑缺失。

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

因此,在Python或Ruby等语言中,参数类型没有调度(或者更确切地说,程序员必须自己实现),因此不需要访问者模式。或者,可以说没有通过visitee方法调度就可以更好地实现访问者模式。

一般情况下,在Python,Ruby或Smalltalk等动态语言中,最好是让#34; visitee"类包含所需的所有信息(此处,动词适用),并在必要时提供钩子以支持"访客",例如命令或策略模式,或使用此处显示的非访问者模式。 / p>

结论

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

使用模式的事情是,不是盲目地复制UML图,而是必须了解他们想要完成什么,以及他们如何通过具体考虑的语言机制来实现这些目标。在这种情况下,实现相同优点的模式看起来不同,并且具有不同的调用模式。这样做可以让你使它们适应不同的语言,但也适用于同一种语言中的不同具体情况。

更新:这是关于实施此模式的红宝石文章:http://blog.rubybestpractices.com/posts/aaronp/001_double_dispatch_dance.html

双重调度似乎对我不利;就我所知,你可以随便取消它。

答案 1 :(得分:1)

这个答案是在忽略PHP等的情况下做出的,但访问者通常需要在实体上调用的不仅仅是一个方法(你提到“序列化”)。当在具体的访问者上调用Visit()方法时,访问者能够为每个实体子类型运行不同的代码。我不知道这与动态类型的语言有什么不同(虽然我喜欢一些反馈)。

Visitor的另一个不错的好处是它提供了一个干净的分离代码,它可以从枚举实体的代码中运行每个实体。这至少在一个大型项目中为我节省了一些严重的代码重复。

顺便说一句,我在没有方法重载的语言中使用了Visitor。您只需使用VisitN(TypeN n)替换Visit(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,if else。

关于处理枚举的访问者,我认为你是这样限制自己的。这并不是说合作类(抽象的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子类以完全解耦它们。希望你能看到保持枚举分离的力量。

答案 2 :(得分:0)

也许,这取决于语言。

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

我喜欢这句好奇的评论,wikipedia说明:

  

Lisp的对象系统[CLOS]及其多次调度不会取代访问者模式,   但只是提供了一个更简洁的实现,其中模式除了   消失。

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

答案 3 :(得分:0)

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

如果我可以使用一系列异构对象并在没有“访问”类的任何合作的情况下调用他们的公共方法,这仍然应该被称为“访问者模式”吗?

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

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

  • 每个访问者都是一组元素(实体)上的算法,可以插入新访问者而无需更改现有代码。开放/封闭原则。
  • 经常添加新元素时,最好避免访问者模式

答案 4 :(得分:0)

访客模式对我来说意味着根据对象的类型为对象添加新功能。显然有if / else梯子来执行类型特定的操作是(我想解释一下这个:()。在python中,我能够做到这一点,没有整个双重派遣剧, monkeypatching (另一个坏主意)某些函数作为类方法。

我问了这个here

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

然后,假设类代码被锁定(或者功能与数据分离)。现在,我有动态分配给类的方法。请注意,在下面的示例中,迭代/递归方法调用名称(stringify)与函数名称( nodeType _stringify)不同。

ASTConst

我可以随时随地扩展类(可能是模块初始化期间的一次)(不好主意?)。

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 将适当调用正确的子方法(递归),而不显式检查类型。

这种技术是不是类似于向Javascript原型添加方法(Visitor pattern in JS)。

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

答案 5 :(得分:0)

访客模式做两件事:

  • 允许ad hoc多态(相同的功能,但做不同的事情 到不同的&#34;类型&#34;)。
  • 允许在不更改数据提供者的情况下添加新的消费算法。

您可以在没有访问者和运行时类型信息的动态语言中做第二。但首先需要一些明确的机制,或设计模式,如访客。