
访问者模式是 GoF 23 种经典设计模式中最难理解的模式之一。因为它难理解、难实现,应用它可能会导致代码的可读性、可维护性变差,所以,访问者模式在实际的软件开发中很少被用到,在没有特别必要的情况下,建议不要使用访问者模式。

假设现在存在两个元素类:
1 | abstract class Element { |
注意到,每个 ConcreteElement 中保存的信息类型并不相同。
给定元素的列表,我们可以打印每个元素的信息:
1 | public static void main(String[] args) { |
假设现在每个元素需要在已有 printInfo() 方法的基础上,增加各种处理 info 的方法。这时候需要为每个类都实现这些新方法,这会导致:
ConcreteElement 中,导致类的职责不单一。下面将使用访问者模式重构:
1 | abstract class Element { |
Visitor 包揽打印元素信息的活,通过重载的方式对不同元素实现不同的打印方法。
打印元素:
1 | public static void main(String[] args) { |
以上代码无法通过编译
visitor.printInfo(e); 中报 Cannot resolve method 'printInfo(Element)" 错误提示。java: 对于printInfo(top.uuanqin.Element), 找不到合适的方法原因:多态是一种动态绑定,可以在运行时获取对象的实际类型,来运行实际类型对应的方法。而函数重载是一种静态绑定,在编译时并不能获取对象的实际类型,而是根据声明类型执行声明类型对应的方法。
我们为每个具体元素实现接收访问者的方法:
1 | abstract class Element { |
遍历元素集合,将访问者传入:
1 | public static void main(String[] args) { |
这里的 Visitor 中的重载函数能被成功调用的关键,在于每个 ConcreteElement 的 accept() 方法接收了访问者,并将自身 this 传递进去。连同着 ConcreteElement 的类型信息传给 Visitor 后,编译器自然会知道应当调用 Visitor 的哪个重载函数。
不要因为看到每个 ConcreteElement 的 accept() 代码似乎都一样就把它提到父类中。如果这样的话, Visitor 重载还是会出现和之前一样的编译错误。
不知道为什么,访问者模式让我想起三借芭蕉扇中,铁扇公主吞下孙悟空的场景。
假设除了每个元素除了 printInfo() 外,还需要新增方法 printInfoType() 用于打印消息类型。一个想法是:
Element 新增抽象接口接收新的 visitor。相应的,所有的 ConcreteElement 都实现这个接口。visitor,编写重载函数 printInfoType()。这种想法存在一些问题,当我们需要添加新的业务时,还是会需要改动到 Element 的代码。针对这个问题,我们抽象出一个 Visitor 接口,包含一个名为 visit() 的重载函数。visit() 是一个通用化的起名方式,不同的 ConcreteVisitor 将会有不同的实现,代表不同的业务。
Visitor 的实现:
1 |
|
Element 的代码在业务增加时不需要改动。注意,此处的 Visitor 是个更为通用的接口:
1 | abstract class Element { |
Main 中的调用方法:
1 | public static void main(String[] args) { |
访问者模式 Visitor Design Pattern
Allows for one or more operation to be applied to a set of objects at runtime, decoupling the operations from the object structure.——GoF
允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。
在访问者模式中,数据结构与处理被分离开来。我们编写一个表示「访问者」的类来访问数据结构中的元素,并把对各元素的处理交给访问者类。这样,当需要增加新的处理时,我们只需要编写新的访问者,然后让数据结构可以接受访问者的访问即可。
登场角色:
Visitor(访问者):Visitor 角色负责对数据结构中每个具体的元素(ConcreteElement 角色)声明一个用于访问 Xxxxx 的 visit(Xxxxx) 方法。visit(Xxxxx) 是用于处理 Xxxxx 的方法,负责实现该方法的是 ConcreteVisitor 角色。ConcreteVisitor(具体的访问者):ConcreteVisitor 角色负责实现 Visitor 角色所定义的接口(API)。它要实现所有的 visit(Xxxxx) 方法,即实现如何处理每个 ConcreteElement 角色。具体访问者知晓具体元素。Element(元素):Element 角色表示 Visitor 角色的访问对象。它声明了接受访问者的 accept 方法。accept 方法接收到的参数是 Visitor 角色。ConcreteElement(具体元素):ConcreteElement 角色负责实现 Element 角色所定义的接口(API)。ObjectStructure(对象结构):ObjectStructure 角色负责处理 Element 角色的集合。ConcreteVisitor 角色为每个 Element 角色都准备了处理方法。一般来说,访问者模式针对的是一组类型不同的对象 ConcreteElement。不过,尽管这组对象的类型是不同的,但是,它们继承相同的父类 Element 或者实现相同的接口。在不同的应用场景下,我们需要对这组对象进行一系列不相关的业务操作,但为了避免不断添加功能导致 ConcreteElement 不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,我们使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者 ConcreteVisitor 中。
Visitor 工作条件:Element 角色必须向 Visitor 角色公开足够多的信息。缺点就是,如果公开了不应当被公开的信息,将来对数据结构的改良就会变得非常困难。
| 优点 | 缺点 |
|---|---|
| 开闭原则。可以引入在不同类对象上执行的新行为,且无需对这些类做出修改。 | 难以增加 ConcreteElement 角色。每次在元素层次结构中添加或移除一个类时,你都要更新所有的访问者。 |
| 单一职责原则。可将同一行为的不同版本移到同一个类中。 | 在访问者同某个元素进行交互时,它们可能没有访问元素私有成员变量和方法的必要权限。如果公开了不应当被公开的信息,将来对数据结构的改良就会变得非常困难。 |
| 访问者对象可以在与各种对象交互时收集一些有用的信息。当你想要遍历一些复杂的对象结构 (例如对象树),并在结构中的每个对象上应用访问者时,这些信息可能会有所帮助。 | |
易于增加 ConcreteVisitor 角色。 |
拓展思路:
ConcreteVisitor,不易扩展被访问者 ConcreteElement。新增 ConcreteElement 后,需要在所有 ConcreteVisitor 中添加相应的重载方法。其他相关设计模式:
在本文首章中提到的案例是访问者模式的简化版,如果业务功能并不多,我们其实可以使用 站内文章工厂模式。
1 | package top.uuanqin.dispatch; |
当需要增加功能时,如 printInfoType,我们需要增加:
InfoTypePrinterFactoryInfoTypePrinterElementAInfoTypePrinter、ElementBInfoTypePrinter单分派和双分派:
| 单分派 Single Dispatch | 双分派 Double Dispatch | |
|---|---|---|
| 执行哪个对象 | 根据对象 运行时类型 决定 | 根据对象的 运行时类型 来决定 |
执行对象 A 的哪个方法 |
根据方法参数的 编译时类型 来决定 | 根据方法参数的 运行时类型 来决定 |
| 语言 | Java、C++、C# |
在面向对象编程语言中,我们可以把方法调用理解为一种消息传递,也就是「分派 Dispatch」。一个对象调用另一个对象的方法,就相当于给它发送一条消息。这条消息起码要包含对象名、方法名、方法参数。
具体到访问者模式中,
element接受visitor,而visitor又访问element。ConcreteElement和ConcreteVisitor这两个角色共同决定了实际进行的处理。这种消息分发的方式一般被称为双重分发(Double dispatch)。
所谓「单」「双」,指的是执行哪个对象的哪个方法,跟几个因素的运行时类型有关。具体到编程语言的语法机制,单分派和双分派和多态、函数重载直接相关。以 Java 为例,Java 支持多态特性,代码可以在运行时获得对象的实际类型(运行时类型),然后根据实际类型决定调用哪个方法。尽管 Java 支持函数重载,但 Java 设计的函数重载的语法规则时,并不是在运行时根据传递进函数的参数的实际类型来决定调用哪个重载函数,而是在编译时根据传递进函数的参数的声明类型(编译时类型)来决定调用哪个重载函数。
1 | public class DispatchTest { |
代码执行结果:
1 | 示例1: |
访问者模式使用了双分派的技巧,让单分派的语言选择正确的类并执行正确的方法。尽管访问者模式基于双分派的原则创建,但这并不是其主要目的。访问者的目的是让你能为整个类层次结构添加「外部」操作,而无需修改这些类的已有代码。

访问者模式是 GoF 23 种经典设计模式中最难理解的模式之一。因为它难理解、难实现,应用它可能会导致代码的可读性、可维护性变差,所以,访问者模式在实际的软件开发中很少被用到,在没有特别必要的情况下,建议不要使用访问者模式。

假设现在存在两个元素类:
1 | abstract class Element { |
注意到,每个 ConcreteElement 中保存的信息类型并不相同。
给定元素的列表,我们可以打印每个元素的信息:
1 | public static void main(String[] args) { |
假设现在每个元素需要在已有 printInfo() 方法的基础上,增加各种处理 info 的方法。这时候需要为每个类都实现这些新方法,这会导致:
ConcreteElement 中,导致类的职责不单一。下面将使用访问者模式重构:
1 | abstract class Element { |
Visitor 包揽打印元素信息的活,通过重载的方式对不同元素实现不同的打印方法。
打印元素:
1 | public static void main(String[] args) { |
以上代码无法通过编译
visitor.printInfo(e); 中报 Cannot resolve method 'printInfo(Element)" 错误提示。java: 对于printInfo(top.uuanqin.Element), 找不到合适的方法原因:多态是一种动态绑定,可以在运行时获取对象的实际类型,来运行实际类型对应的方法。而函数重载是一种静态绑定,在编译时并不能获取对象的实际类型,而是根据声明类型执行声明类型对应的方法。
我们为每个具体元素实现接收访问者的方法:
1 | abstract class Element { |
遍历元素集合,将访问者传入:
1 | public static void main(String[] args) { |
这里的 Visitor 中的重载函数能被成功调用的关键,在于每个 ConcreteElement 的 accept() 方法接收了访问者,并将自身 this 传递进去。连同着 ConcreteElement 的类型信息传给 Visitor 后,编译器自然会知道应当调用 Visitor 的哪个重载函数。
不要因为看到每个 ConcreteElement 的 accept() 代码似乎都一样就把它提到父类中。如果这样的话, Visitor 重载还是会出现和之前一样的编译错误。
不知道为什么,访问者模式让我想起三借芭蕉扇中,铁扇公主吞下孙悟空的场景。
假设除了每个元素除了 printInfo() 外,还需要新增方法 printInfoType() 用于打印消息类型。一个想法是:
Element 新增抽象接口接收新的 visitor。相应的,所有的 ConcreteElement 都实现这个接口。visitor,编写重载函数 printInfoType()。这种想法存在一些问题,当我们需要添加新的业务时,还是会需要改动到 Element 的代码。针对这个问题,我们抽象出一个 Visitor 接口,包含一个名为 visit() 的重载函数。visit() 是一个通用化的起名方式,不同的 ConcreteVisitor 将会有不同的实现,代表不同的业务。
Visitor 的实现:
1 |
|
Element 的代码在业务增加时不需要改动。注意,此处的 Visitor 是个更为通用的接口:
1 | abstract class Element { |
Main 中的调用方法:
1 | public static void main(String[] args) { |
访问者模式 Visitor Design Pattern
Allows for one or more operation to be applied to a set of objects at runtime, decoupling the operations from the object structure.——GoF
允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。
在访问者模式中,数据结构与处理被分离开来。我们编写一个表示「访问者」的类来访问数据结构中的元素,并把对各元素的处理交给访问者类。这样,当需要增加新的处理时,我们只需要编写新的访问者,然后让数据结构可以接受访问者的访问即可。
登场角色:
Visitor(访问者):Visitor 角色负责对数据结构中每个具体的元素(ConcreteElement 角色)声明一个用于访问 Xxxxx 的 visit(Xxxxx) 方法。visit(Xxxxx) 是用于处理 Xxxxx 的方法,负责实现该方法的是 ConcreteVisitor 角色。ConcreteVisitor(具体的访问者):ConcreteVisitor 角色负责实现 Visitor 角色所定义的接口(API)。它要实现所有的 visit(Xxxxx) 方法,即实现如何处理每个 ConcreteElement 角色。具体访问者知晓具体元素。Element(元素):Element 角色表示 Visitor 角色的访问对象。它声明了接受访问者的 accept 方法。accept 方法接收到的参数是 Visitor 角色。ConcreteElement(具体元素):ConcreteElement 角色负责实现 Element 角色所定义的接口(API)。ObjectStructure(对象结构):ObjectStructure 角色负责处理 Element 角色的集合。ConcreteVisitor 角色为每个 Element 角色都准备了处理方法。一般来说,访问者模式针对的是一组类型不同的对象 ConcreteElement。不过,尽管这组对象的类型是不同的,但是,它们继承相同的父类 Element 或者实现相同的接口。在不同的应用场景下,我们需要对这组对象进行一系列不相关的业务操作,但为了避免不断添加功能导致 ConcreteElement 不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,我们使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者 ConcreteVisitor 中。
Visitor 工作条件:Element 角色必须向 Visitor 角色公开足够多的信息。缺点就是,如果公开了不应当被公开的信息,将来对数据结构的改良就会变得非常困难。
| 优点 | 缺点 |
|---|---|
| 开闭原则。可以引入在不同类对象上执行的新行为,且无需对这些类做出修改。 | 难以增加 ConcreteElement 角色。每次在元素层次结构中添加或移除一个类时,你都要更新所有的访问者。 |
| 单一职责原则。可将同一行为的不同版本移到同一个类中。 | 在访问者同某个元素进行交互时,它们可能没有访问元素私有成员变量和方法的必要权限。如果公开了不应当被公开的信息,将来对数据结构的改良就会变得非常困难。 |
| 访问者对象可以在与各种对象交互时收集一些有用的信息。当你想要遍历一些复杂的对象结构 (例如对象树),并在结构中的每个对象上应用访问者时,这些信息可能会有所帮助。 | |
易于增加 ConcreteVisitor 角色。 |
拓展思路:
ConcreteVisitor,不易扩展被访问者 ConcreteElement。新增 ConcreteElement 后,需要在所有 ConcreteVisitor 中添加相应的重载方法。其他相关设计模式:
在本文首章中提到的案例是访问者模式的简化版,如果业务功能并不多,我们其实可以使用 站内文章工厂模式。
1 | package top.uuanqin.dispatch; |
当需要增加功能时,如 printInfoType,我们需要增加:
InfoTypePrinterFactoryInfoTypePrinterElementAInfoTypePrinter、ElementBInfoTypePrinter单分派和双分派:
| 单分派 Single Dispatch | 双分派 Double Dispatch | |
|---|---|---|
| 执行哪个对象 | 根据对象 运行时类型 决定 | 根据对象的 运行时类型 来决定 |
执行对象 A 的哪个方法 |
根据方法参数的 编译时类型 来决定 | 根据方法参数的 运行时类型 来决定 |
| 语言 | Java、C++、C# |
在面向对象编程语言中,我们可以把方法调用理解为一种消息传递,也就是「分派 Dispatch」。一个对象调用另一个对象的方法,就相当于给它发送一条消息。这条消息起码要包含对象名、方法名、方法参数。
具体到访问者模式中,
element接受visitor,而visitor又访问element。ConcreteElement和ConcreteVisitor这两个角色共同决定了实际进行的处理。这种消息分发的方式一般被称为双重分发(Double dispatch)。
所谓「单」「双」,指的是执行哪个对象的哪个方法,跟几个因素的运行时类型有关。具体到编程语言的语法机制,单分派和双分派和多态、函数重载直接相关。以 Java 为例,Java 支持多态特性,代码可以在运行时获得对象的实际类型(运行时类型),然后根据实际类型决定调用哪个方法。尽管 Java 支持函数重载,但 Java 设计的函数重载的语法规则时,并不是在运行时根据传递进函数的参数的实际类型来决定调用哪个重载函数,而是在编译时根据传递进函数的参数的声明类型(编译时类型)来决定调用哪个重载函数。
1 | public class DispatchTest { |
代码执行结果:
1 | 示例1: |
访问者模式使用了双分派的技巧,让单分派的语言选择正确的类并执行正确的方法。尽管访问者模式基于双分派的原则创建,但这并不是其主要目的。访问者的目的是让你能为整个类层次结构添加「外部」操作,而无需修改这些类的已有代码。