访问者模式中的accept()方法的要点是什么?

关于从类中去除algorithm的讨论有很多。 但是,有一件事暂时没有解释。

他们使用这样的访客

abstract class Expr { public <T> T accept(Visitor<T> visitor) {visitor.visit(this);} } class ExprVisitor extends Visitor{ public Integer visit(Num num) { return num.value; } public Integer visit(Sum sum) { return sum.getLeft().accept(this) + sum.getRight().accept(this); } public Integer visit(Prod prod) { return prod.getLeft().accept(this) * prod.getRight().accept(this); } 

访问者不是直接调用visit(element),而是要求该元素调用其访问方法。 它与class级对游客不了解的观点相矛盾。

PS1请用你自己的话来解释或指出确切的解释。 因为我得到的两个回答是指一些普遍而不确定的东西。

PS2我的猜测:因为getLeft()返回基本Expression ,所以调用visit(getLeft())将导致visit(Expression) ,而getLeft()调用visit(this)将导致另一个更合适的访问调用。 所以, accept()执行types转换(又名铸造)。

PS3的斯卡拉的模式匹配=访问者模式在类固醇显示访问者模式没有接受方法是多么简单。 维基百科增加了这个声明 :通过链接一个文件显示“ accept()方法是不必要的,当reflection是可用的;引入术语”Walkabout“的技术。

访问者模式的visit / accept构造是类C语言(C#,Java等)语义的一个必要的邪恶。 访问者模式的目标是使用双重调度路由您的呼叫,正如您期望通过阅读代码。

通常,当使用访问者模式时,涉及对象层次结构,其中所有节点都是从基本Nodetypes派生的,此后称为Node 。 本能地,我们会这样写:

 Node root = GetTreeRoot(); new MyVisitor().visit(root); 

这就是问题所在。 如果我们的MyVisitor类定义如下:

 class MyVisitor implements IVisitor { void visit(CarNode node); void visit(TrainNode node); void visit(PlaneNode node); void visit(Node node); } 

如果在运行时,不pipe实际types是什么,那么我们的调用将进入过载visit(Node node) 。 所有声明为Nodetypes的variables都是如此。 为什么是这样? 由于Java和其他类C语言在决定调用哪个超载时只考虑参数的静态types或variables声明的types。 对于每个方法调用,Java在运行时并没有采取额外的步骤:“好的, root的dynamictypes是什么?哦,我明白了,它是一个TrainNode 。我们来看看TrainNode是否有方法接受TrainNodetypes的参数…“。 编译器在编译时确定哪个方法将被调用。 (如果Java确实检查了参数的dynamictypes,性能会非常糟糕。)

Java为我们提供了一种工具,用于在调用方法时考虑对象的运行时(即dynamic)types – 虚拟方法调度 。 当我们调用一个虚拟方法时,调用实际上会转到内存中的一个由函数指针组成的表中。 每种types都有一张桌子。 如果一个特定的方法被一个类所覆盖,这个类的函数表项将包含被重写的函数的地址。 如果这个类不覆盖一个方法,它将包含一个指向基类实现的指针。 这仍然会导致性能开销(每个方法调用基本上是取消引用两个指针:一个指向types的函数表,另一个是函数本身),但是它比检查参数types更快。

访问者模式的目标是完成双重调度 – 不仅是调用目标的types( MyVisitor ,通过虚拟方法),还有参数的types(我们正在查看的是哪种types的Node )? 访问者模式允许我们通过visit / accept组合来完成此操作。

通过改变我们的行:

 root.accept(new MyVisitor()); 

我们可以得到我们想要的:通过虚拟方法调度,我们input正确的accept()调用,由子类实现 – 在我们的TrainElement示例中,我们将inputTrainElementaccept()

 class TrainNode extends Node implements IVisitable { void accept(IVisitor v) { v.visit(this); } } 

TrainNodeaccept范围内,编译器知道什么? 它知道this是一个TrainNode的静态types 。 编译器在调用者的范围内没有意识到这是一个重要的额外的信息碎片:在那里,它所知道的root就是它是一个Node 。 现在编译器知道thisroot )不仅仅是一个Node ,而是一个TrainNode 。 因此,在accept()v.visit(this)find的一行代表完全是其他的意思。 编译器现在会寻找一个visit()的重载,它需要一个TrainNode 。 如果找不到,则会将该调用编译为带有Node的重载。 如果两者都不存在,你将得到一个编译错误(除非你有一个重载object )。 因此执行将进入我们一直以来的目标: MyVisitor实现visit(TrainNode e) 。 不需要施工,最重要的是,不需要反思。 因此,这种机制的开销是相当低的:它只包含指针引用,没有别的。

你的问题是正确的 – 我们可以使用一个强制转换并获得正确的行为。 但是,我们经常不知道Node是什么types的。 采取以下层次的情况:

 abstract class Node { ... } abstract class BinaryNode extends Node { Node left, right; } abstract class AdditionNode extends BinaryNode { } abstract class MultiplicationNode extends BinaryNode { } abstract class LiteralNode { int value; } 

我们正在编写一个简单的编译器来分析一个源文件,并生成一个符合上述规范的对象层次结构。 如果我们正在为访问者实现层次结构的解释器:

 class Interpreter implements IVisitor<int> { int visit(AdditionNode n) { int left = n.left.accept(this); int right = n.right.accept(this); return left + right; } int visit(MultiplicationNode n) { int left = n.left.accept(this); int right = n.right.accept(this); return left * right; } int visit(LiteralNode n) { return n.value; } } 

铸造不会让我们走得很远,因为我们不知道visit()方法中leftright的types。 我们的parsing器很可能也会返回一个指向层次结构根节点的Nodetypes的对象,所以我们也不能安全地进行转换。 所以我们简单的解释器可以看起来像:

 Node program = parse(args[0]); int result = program.accept(new Interpreter()); System.out.println("Output: " + result); 

访问者模式允许我们做一些非常强大的事情:给定一个对象层次结构,它允许我们创build模块化操作,可以在层次结构上运行,而不需要将代码放入层次结构的类本身。 访问者模式在编译器构造中被广泛使用。 考虑到特定程序的语法树,许多访问者被编写在该树上:types检查,优化,机器码发射通常被实现为不同的访问者。 在优化访问者的情况下,甚至可以输出给定input树的新语法树。

当然,它有其缺点:如果我们在层次结构中添加一个新types,我们还需要为这个新types添加一个visit()方法到IVisitor接口中,并且在所有访问者中创build存根(或全部)实现。 我们还需要添加accept()方法,出于上述原因。 如果性能对你来说意义不大,那么写访问者的解决scheme不需要accept() ,但是它们通常涉及reflection,因此可能会产生相当大的开销。

当然,如果这是“接受”实施的唯一方式,那将是愚蠢的。

但事实并非如此。

例如,访问者在处理层次结构时非常有用 ,在这种情况下,非terminal节点的实现可能是这样的

 interface IAcceptVisitor<T> { void Accept(IVisit<T> visitor); } class HierarchyNode : IAcceptVisitor<HierarchyNode> { public void Accept(IVisit<T> visitor) { visitor.visit(this); foreach(var n in this.children) n.Accept(visitor); } private IEnumerable<HierarchyNode> children; .... } 

你看? 你所描述的愚蠢是遍历层次结构解决scheme。

这是一个更长,更深入的文章,让我了解访问者 。

编辑:澄清:访问者的Visit方法包含应用于节点的逻辑。 节点的Accept方法包含如何导航到相邻节点的逻辑。 只有双倍调度的情况是一种特殊情况,其中不存在相邻节点的导航。

访客模式的目的是确保对象知道访问者何时完成并已经离开,所以class级可以在之后进行必要的清理。 它也允许类将“内部”暂时作为“ref”参数公开,并且知道一旦访问者离开后,内部将不再被暴露。 在不需要清理的情况下,访问者模式并不是非常有用。 没有这些东西的类可能不会受益于访问者模式,但是使用访问者模式编写的代码将可用于将来可能需要在访问后进行清理的类。

例如,假设有一个数据结构包含许多应该primefaces更新的string,但是持有该数据结构的类不知道应该执行哪种types的primefaces更新(例如,如果一个线程想要replace所有出现的“ X“,而另一个线程想要用一个数值更高的序列来replace任何数字序列,则两个线程的操作都应该成功;如果每个线程只是简单地读出一个string,执行更新并将其写回,则第二个线程写回它的string会覆盖第一个)。 一种方法是让每个线程获得一个锁,执行它的操作并释放锁。 不幸的是,如果锁以这种方式暴露出来,数据结构将无法阻止某人获得锁而永远不会释放它。

访问者模式提供(至less)三种方法来避免这个问题:

  1. 它可以lockinglogging,调用提供的function,然后解锁logging; 如果提供的函数陷入无限循环,则logging可以永久locking,但是如果提供的函数返回或抛出exception,logging将被解锁(如果函数抛出exception,则logging无效是合理的;它locking可能不是一个好主意)。 请注意,如果被调用函数尝试获取其他locking,则可能导致死锁。
  2. 在某些平台上,它可以通过一个存储位置来保存string作为'ref'参数。 然后该函数可以复制该string,根据复制的string计算新的string,尝试CompareExchange旧string到新的string,并在CompareExchange失败时重复整个过程。
  3. 它可以复制string,调用string上提供的函数,然后使用CompareExchange本身来尝试更新原始数据,并在CompareExchange失败时重复整个过程。

如果没有访问者模式,执行primefaces更新将需要公开锁和冒失败的风险,如果调用软件无法遵循严格的locking/解锁协议。 使用访问者模式,可以相对安全地完成primefaces更新。

源代码编译就是一个很好的例子:

 interface CompilingVisitor { build(SourceFile source); } 

客户可以实现一个JavaBuilderRubyBuilderXMLValidator等,并且用于收集和访问项目中所有源文件的实现不需要改变。

如果每个源文件types都有单独的类,这将是一个不好的模式:

 interface CompilingVisitor { build(JavaSourceFile source); build(RubySourceFile source); build(XMLSourceFile source); } 

这涉及到上下文以及您希望可扩展的系统的哪些部分。