您什么时候决定为您的对象使用访问者?
我一直认为一个对象需要数据和消息来对其进行操作。什么时候你需要一个对象外部的方法?您会遵循什么经验法则来接待访客?这是假设您完全控制对象图。
I always thought an object needs the data and the messages to act on it. When would you want a method that is extrinsic to the object? What rule of thumb do you follow to have a visitor? This is supposing that you have full control of the object graph.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(9)
有时在一个类中定义特定对象的所有行为并不方便。例如,在 Java 中,如果您的模块需要在最初在另一个模块中定义的一堆类中实现方法
toXml
,那么它会很复杂,因为您无法在其他地方编写toXml
与原始类文件相比,这意味着您无法在不更改现有源的情况下扩展系统(在 Smalltalk 或其他语言中,您可以将不与特定文件绑定的扩展方法分组)。更一般地说,静态类型语言中存在以下能力之间的紧张关系:(1) 向现有数据类型添加新函数,以及 (2) 添加支持相同函数的新数据类型实现——这称为 表达式问题 (维基百科页面)。
面向对象语言在第 2 点上表现出色。如果您有接口,则可以安全、轻松地添加新的实现。函数式语言在第 1 点上表现出色。它们依赖于模式匹配/临时多态性/重载,因此您可以轻松地向现有类型添加新函数。
访问者模式是支持面向对象设计中的第 1 点的一种方式:您可以轻松地以类型安全的方式使用新行为来扩展系统(如果您这样做,情况就不会是这样)与
if-else-instanceof
进行手动模式匹配,因为如果未涵盖案例,该语言永远不会警告您)。当存在一组固定的已知类型时,通常会使用访问者,我认为这就是您所说的“对象图的完全控制”的意思。示例包括解析器中的标记、具有各种类型节点的树以及类似的情况。
总而言之,我想说你的分析是正确的:)
PS:访问者模式与复合模式配合得很好,但它们单独也很有用
It's sometimes not convenient to have all behaviors for a particular object defined in one class. For instance in Java, if your module requires a method
toXml
to be implemented in a bunch of classes originally defined in another module, it's complicated because you can not writetoXml
somewhere else than the original class file, which mean you can not extend the system without changing existing sources (in Smalltalk or other languages, you can group method in extension which are not tied to a particular file).More generally, there's a tension in statically typed languages between the ability to (1) add new functions to existing data types, and (2) add new data types implementations supporting the same functions -- that's called the expression problem (wikipedia page).
Object oriented languages excel at point 2. If you have an interface, you can add new implementations safely and easily. Functional languages excel at point 1. They rely on pattern matching/ad-hoc polymorphism/overloading so you can add new functions to existing types easily.
The visitor pattern is a way to support point 1 in an object-oriented design: you can easily extend the system with new behaviors in a type-safe way (which wouldn't be the case if you do kind of manual pattern matching with
if-else-instanceof
because the language would never warn you if a case is not covered).Visitors are then typically used when there is a fixed set of known types, which I think is what you meant by "full control of the object graph". Examples include token in a parser, tree with various types of nodes, and similar situations.
So to conclude, I would say you were right in your analysis :)
PS: The visitor pattern works well with the composite pattern, but they are also useful individually
当对相当复杂的数据结构的所有元素应用操作时(例如,并行遍历元素,或遍历高度互连的数据结构)或实现双分派,访问者模式特别有用。如果要按顺序处理元素并且不需要双重分派,那么实现自定义 Iterable 和 Iterator 通常是更好的选择,特别是因为它更适合其他 API。
The visitor pattern is particularly useful when applying an operation to all elements of a fairly complicated data structure for which traversal is non-trivial (e.g. traversing over the elements in parallel, or traversing a highly interconnected data structure) or in implementing double-dispatch. If the elements are to be processed sequentially and if double-dispatch is not needed, then implementing a custom Iterable and Iterator is usually the better choice, especially since it fits in better with the other APIs.
有时这只是组织问题。如果您有 n 种对象(即:类)和 m 种操作(即:方法),您是否希望将 n * m 类/方法对按类或方法分组?大多数面向对象语言强烈倾向于按类对事物进行分组,但在某些情况下按操作进行组织更有意义。例如,在对象图的多阶段处理中,例如在编译器中,将每个阶段(即:操作)视为一个单元通常比考虑特定类型可能发生的所有操作更有用的节点。
访问者模式的一个常见用例不仅仅是严格组织,而是打破不需要的依赖关系。例如,通常不希望您的“数据”对象依赖于表示层,特别是当您想象可能有多个表示层时。通过使用访问者模式,表示层的详细信息存在于访问者对象中,而不是存在于数据对象的方法中。数据对象本身只知道抽象访问者接口。
Sometimes its just a matter of organization. If you have n-kinds of objects (ie: classes) with m-kinds of operations (ie: methods), do you want to have the n * m class/method pairs to be grouped by class or by method? Most OO languages strongly lean towards having things grouped by class, but there are cases where organizing by operation makes more sense. For example, in a multi-phase processing of object graphs, like in a compiler, is often more useful to think about each phase (ie: operation) as a unit rather than to think about all of operations that can happen to a particular sort of node.
A common use-case for the Visitor pattern where it's more than just strictly organizational is to break unwanted dependencies. For example, it's generally undesirable to have your "data" objects depend on your presentation layer, especially if you imagine that you may have multiple presentation layers. By using the visitor pattern, details of the presentation layer live in the visitor objects, not in methods of the data objects. The data objects themselves only know about the abstract visitor interface.
当我发现我想将一个有状态的方法放到 Entity/DataObject/BusinessObject 上时,我经常使用它,但我真的不想将这种有状态引入到我的对象中。有状态访问者可以完成这项工作,或者从我的无状态数据对象生成有状态执行器对象的集合。当工作的处理将被外包给执行程序线程时特别有用,许多有状态访问者/工作人员可以引用同一组无状态对象。
I use it a lot when I find I want to put a method that will be stateful onto Entity/DataObject/BusinessObject but I really don't want to introduce that statefulness to my object. A stateful visitor can do the job, or generate a collection of stateful executor objects from my non-stateful data objects. Particularly useful when processing of the work is going to be farmed out to executor threads, many stateful visitor/workers can reference the same group of non-stateful objects.
对我来说,使用访问者模式的唯一原因是当我需要对树/特里树等图形数据结构执行双重调度时。
For me, the only one reason to use visitor pattern is when I need to perform double dispatch on graph-like data structure like tree/trie.
当您遇到以下问题时:
然后,您可以使用具有以下意图之一的访问者模式:
(来自 http://sourcemaking.com/design_patterns/visitor)
When you have the following problem:
Then you can use a Visitor pattern with one of the following intents:
(From http://sourcemaking.com/design_patterns/visitor)
当您需要根据对象类型(在类层次结构中)改变行为并且可以根据对象提供的公共接口来定义该行为时,访问者模式最有用。该行为不是该对象固有的,并且不会从对象中受益或不需要对象的封装。
我发现访问者通常会自然而然地出现对象的图形/树,其中每个节点都是类层次结构的一部分。为了允许客户端遍历图/树并以统一的方式处理任何每种类型的节点,访问者模式实际上是最简单的替代方案。
例如,考虑一个 XML DOM - 节点是基类,元素、属性和其他类型的节点定义类层次结构。
想象一下需求是将 DOM 作为 JSON 输出。该行为不是 Node 固有的 - 如果是的话,我们必须向 Node 添加方法来处理客户端可能需要的所有格式(
toJSON()
、toASN1()、
toFastInfoSet()
等)我们甚至可以认为toXML()
不属于那里,尽管这可能是为了方便而提供的,因为它将是大多数客户端都使用它,并且在概念上“更接近”DOM,因此为了方便起见,可以将 toXML 内置于 Node 中 - 尽管它不是必须的,并且可以像所有其他格式一样进行处理。由于 Node 及其子类使其状态完全可用作方法,因此我们拥有外部所需的所有信息,以便能够将 DOM 转换为某种输出格式。我们可以使用 Visitor 接口,在 Node 上使用抽象
accept()
方法,并在每个子类中实现,而不是将输出方法放在 Node 对象上。每个访问者方法的实现处理每个节点类型的格式化。它可以做到这一点,因为所需的所有状态都可以从每个节点类型的方法中获得。
通过使用访问者,我们打开了实现任何所需输出格式的大门,而无需为每个 Node 类增加该功能的负担。
The visitor pattern is most useful when you need behaviour to vary by object type (in a class hierarchy), and that behaviour can be defined in terms of the public interface provided by the object. The behaviour is not intrinsic to that object and doesn't benefit from or require encapsulation by the object.
I find visitors often naturally arises with graphs/trees of objects, where each node is part of a class hierarchy. To allow clients to walk the graph/tree and handle any each type of node in a uniform way, the Visitor pattern is really the simplest alternative.
For example, consider an XML DOM - a Node is the base class, with Element, Attribute and other types of Node define the class hierarchy.
Imagine the requirement is to output the DOM as JSON. The behaviour is not intrinsic to a Node - if it were, we would have to add methods to Node to handle all formats that the client might need (
toJSON()
,toASN1()
,toFastInfoSet()
etc.) We could even argue thattoXML()
doesn't belong there, although that might be provided for convenience since it is going to be used by most clients, and is conceptually "closer" to the DOM, so toXML could be made intrinsic to Node for convenience - although it doesn't have to be, and could be handled like all the other formats.As Node and its subclasses make their state fully available as methods, we have all the information needed externally to be able to convert the DOM to some output format. Instead of then putting the output methods on the Node object, we can use a Visitor interface, with an abstract
accept()
method on Node, and implementation in each subclass.The implementation of each visitor method handles the formatting for each node type. It can do this because all the state needed is available from the methods of each node type.
By using a visitor, we open the door to implementing any output format desired without needing to burden each Node class with that functionality.
当您完全了解实现接口的类时,我总是建议使用访问者。通过这种方式,您将不会执行任何不那么漂亮的
instanceof
调用,并且代码变得更具可读性。此外,一旦实现了访问者,就可以在现在和将来的许多地方重复使用。I would always recommend using a visitor when you have full knowledge of what classes that implement an interface. In this way you won't do any not-so-pretty
instanceof
-calls, and the code becomes a lot more readable. Also, once a visitor has been implemented can be reused in many places, present and future.访问者模式是双重调度问题的一个非常自然的解决方案。双重调度问题是动态调度问题的一个子集,它源于这样一个事实:方法重载是在编译时静态确定的,这与虚拟(重写)方法不同,后者是在运行时确定的。
考虑这种情况:
下面的代码来自 我之前的回答 并翻译成 Java。该问题与上面的示例有些不同,但展示了访问者模式的本质。
Visitor pattern is a very natural solution to double dispatch problems. Double dispatch problem is a subset of dynamic dispatch problems and it stems from the fact that method overloads are determined statically at compile time, unlike virtual(overriden) methods, which are determined at runtime.
Consider this scenario:
This code below is adopted from my previous answer and translated to Java. The problem is somewhat different from the above sample, but demonstrates the essence of Visitor pattern.