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

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

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

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

因此,似乎在python,ruby或php中我可以在访问对象中没有接受方法(并且没有reflection)的情况下实现类似访问者的类,对吧? 如果我可以使用一系列异构对象并在没有“访问”类的任何合作的情况下调用他们的公共方法,那么这仍然应该被称为“访问者模式”吗? 是否存在一些我缺失的模式的本质,或者它是否只是归结为“编写一个从外部操作对象来执行操作的新类”?

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

这个答案是由于对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子类以完全解耦它们。 希望你能看到保持枚举分离的力量。

访问者特别有用的地方是访问者需要切换访问者类型的地方,无论出于何种原因,您不希望将这些知识编码到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 

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

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

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

通常在Python,Ruby或Smalltalk等动态语言中,最好让“visitee”类包含所需的所有信息(此处,动词适用),并在必要时提供钩子以支持“访问者”,例如作为命令或策略模式,或使用此处显示的非访问者模式。

结论

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

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

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

双重派遣似乎是强迫我的; 据我所知,你可以随便取消它。

我认为你可以互换地使用Visitor Pattern和Double Dispatch。 当你说,

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

写一个新的类,从外面操纵你的对象来执行一个操作“?

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

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

也许,这取决于语言。

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

我喜欢这篇关于维基百科的好评,说明:

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

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

访问者模式对我来说意味着根据对象的类型为对象添加新function。 显然有if / else梯子来执行类型特定的操作是不好的 (我想对此进行解释:()。在python中,我能够做到这一点,没有整个双重派遣戏剧,通过monkeypatching (另一个坏主意)确定作为类方法。

我在这里问过这件事。

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

然后,假设类代码被锁定(或者function与数据分离)。 现在,我有动态分配给类的方法。 请注意,在下面的示例中,迭代/递归方法调用名称(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中,不需要使用double-dispatch( VisitorObject.visit(ConcreteObject)调用ConcreteObject.Accept(VisitorObject) )。 也许有人会将这种forms化为动态类型语言,我们手头有新模式,或者没有。 毕竟,模式被发现,而不是发明 (我不记得我在哪里读到这个)。

访客模式做两件事:

  • 允许ad hoc多态(相同的function,但对不同的“类型”做不同的事情)。
  • 允许添加新的消费算法而无需更改数据提供者。

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