10

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

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

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

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

PS。我看过很多关于 SO 和其他地方的讨论,但找不到任何可以解决这个问题的东西。欢迎指点。

4

6 回答 6

2

访问者特别有用的地方是访问者需要打开访问者类型的地方,无论出于何种原因,您都不想将这些知识编码到访问者中(想想插件架构)。考虑以下 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”类携带所需的所有信息(此处为动词适用),并在必要时提供支持“visitor”的钩子,例如作为命令或策略模式,或使用此处显示的非访客模式。

结论

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

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

更新:这里有一篇关于实现这种模式的红宝石文章:http: //blog.rubybestpractices.com/posts/aaronp/001_double_dispatch_dance.html

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

于 2015-04-17T10:48:54.237 回答
1

这个答案是在对 PHP 等一无所知的情况下做出的,但是访问者通常需要在实体上调用的不仅仅是一个方法(你提到了“序列化”)。当在具体的访问者上调用 Visit() 方法时,访问者能够为每个实体子类型运行不同的代码。我看不出这与动态类型语言有何不同(尽管我希望得到一些反馈)。

Visitor 的另一个好处是,它提供了在每个实体上运行的代码与枚举实体的代码的清晰分离。这至少在一个大型项目中为我节省了一些严重的代码重复。

顺便说一句,我在没有方法重载的语言中使用了访问者。您只需将 Visit(TypeN n) 替换为 VisitN(TypeN n)。


从评论跟进。

这是一个访问者伪代码,如果没有访问对象的合作(至少没有 switch 块),我不知道该怎么做:

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
   }

}

访问者基础结构允许处理各种命令子类型,而无需选择 case、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
   }
}

在实际代码中,我将为枚举器创建一个基类,然后将其子类化以处理不同的枚举场景,同时传入具体的访问者子类以完全解耦它们。希望您能看到保持枚举分开的力量。

于 2012-06-22T13:24:41.880 回答
0

也许,这取决于语言。

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

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

Lisp 的对象系统 [CLOS] 及其多重分派并没有取代访问者模式,而只是提供了一个更简洁的实现,其中模式几乎消失了。

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

于 2012-06-22T11:19:12.313 回答
0

我认为您正在交替使用访问者模式和双重调度。当你说,

如果我可以使用一系列异构对象并调用它们的公共方法而无需“访问”类的任何合作,这还值得称为“访问者模式”吗?

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

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

  • 每个访问者都是一组元素(实体)的算法,可以插入新访问者而无需更改现有代码。开/关原则。
  • 频繁添加新元素时,最好避免访问者模式
于 2012-06-22T12:06:40.717 回答
0

对我来说,访问者模式意味着根据对象的类型向对象添加新功能。显然有 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)可能有人会为动态类型语言形式化它,我们手头会有一个新模式,或者没有。毕竟,模式是被发现的,而不是发明的(我不记得我在哪里读到的)。

于 2016-10-01T02:01:56.807 回答
0

访问者模式做两件事:

  • 允许临时多态性(相同的功能,但对不同的“类型”做不同的事情)。
  • 无需更改数据提供者即可添加新的消费算法。

您可以在没有访问者或运行时类型信息的情况下使用动态语言进行第二次操作。但是第一个需要一些明确的机制,或者像访问者这样的设计模式。

于 2017-11-23T06:38:57.893 回答