文章目录
简介
访问者是一种行为设计模式,它能把算法跟他所作用的对象隔离开来。
场景
假如你的团队开发了一款能够使用图像里地理信息的应用程序。图像中的每个节点既能代表复杂实体(例如一座城市),也能代表更精细的对象(例如工业区和旅游景点等)。每个节点的类型都由它所属的类来表示,每个特定的节点就是一个对象。

现在,你需要把图像导出到 XML 文件里。你打算为每个节点类添加导出函数,然后递归执行图像中每个节点的导出函数,有点类似我们之前的组合模式。另外,还可以使用多态让导出方法的调用代码不和具体的节点类相耦合。
但其他同事并不想修改已有的节点类。他们认为这些代码已经是产品了,修改的话可能会引入缺陷。
另外,他们觉得在节点类里包含导出 XML 文件的代码好像没有意义。这些类的主要工作是处理地理数据。加入导出 XML 文件的代码明显不合适。
还有,这个功能完成之后,很有可能还需要提供导出其他类型文件的功能,或者其他奇怪的功能。这样你很可能要再次修改这些脆弱的类。
解决
访问者模式建议把新功能(行为)放在一个叫做访问者的独立类里,而不是整合到已有类里。需要执行操作的原始对象将作为参数传给访问者的方法,让方法能访问对象所包含的一切必要数据。
对于不同类的对象(比如示例里不同的节点),该怎么操作? 在我们的示例里,不同节点类导出 XML 文件的实际实现很可能不同。因此,访问者类可以定义一组方法,每个方法可接收特定类型的参数,如下所示:
java
class ExportVisitor implements Visitor {
method doForCity(City c) { ... }
method doForIndustry(Industry f) { ... }
method doForSightSeeing(SightSeeing ss) { ... }
}
但我们究竟应该怎么调用这些方法(尤其是在处理整个图像方面)呢?这些方法的签名各不相同,我们就不能用多态机制。为了可以挑选出能够处理特定对象的访问者方法,我们需要先对它的类进行检查。是不是很麻烦?
java
foreach (Node node in graph) {
if (node instanceof City)
exportVisitor.doForCity((City) node)
if (node instanceof Industry)
exportVisitor.doForIndustry((Industry) node)
}
为什么不用方法重载?即用相同的方法名称,不同的参数。 我们无法提前知道节点对象所属的类,所以重载机制没办法执行正确的方法。因为方法会把节点基类作为输入参数的默认类型。
访问者模式可以解决这个问题。它使用了一种叫做双分派的技巧,不使用麻烦的条件语句也可以执行正确的方法。其实,与其让客户端来选择调用特定的方法,不如把选择权委派给 作为参数传递给访问者的对象。因为这个对象知道它自己的类,能更自然地在访问者中选出正确的方法。它们会"接收"一个访问者并且告诉它应该执行的访问者方法。
java
// Client code
foreach (Node node in graph)
node.accept(exportVisitor)
// City
class City is
method accept(Visitor v) is
v.doForCity(this)
// ...
// Industry
class Industry is
method accept(Visitor v) is
v.doForIndustry(this)
// ...
我们还是修改了节点类,但改动很小,而且我们能在后续添加行为时 不需要再次修改节点类的代码。
现在,如果我们抽取出所有访问者的通用接口,所有已有的节点都能跟任何访问者交互了。如果需要引入跟节点相关的某个行为,你只需要实现一个新的访问者类就行了。
完整代码
java
// Visitor 接口定义地理信息操作规范
interface GeoVisitor {
void visitCity(CityElement city); // 城市节点访问接口
void visitIndustry(IndustryElement industry); // 工业区访问接口
void visitSightseeing(SightseeingElement sight); // 景区访问接口
}
// 具象访问者(统计信息导出)
class StatisticsExporter implements GeoVisitor {
@Override
public void visitCity(CityElement city) { // 处理城市数据
System.out.printf("统计城市[%s]: 人口%d万 | ",
city.getName(), city.getPopulation()); // 访问节点属性
System.out.println("GDP " + city.getGDP() + "亿元");
}
@Override
public void visitIndustry(IndustryElement industry) { // 处理工业区
System.out.printf("工业区评估: %s 类型 | 占地面积%.1f平方公里\n",
industry.getIndustryType(), industry.getArea());
}
@Override
public void visitSightseeing(SightseeingElement sight) { // 处理景点
System.out.printf("景点分级: %s(%dA景区)\n",
sight.getLandmark(), sight.getRating());
}
}
// 地理元素抽象接口(使用双分派机制)
interface GeoElement {
void accept(GeoVisitor visitor); // 关键接收方法
}
// 城市实体类
class CityElement implements GeoElement {
private String name;
private int population;
private double gdp;
public CityElement(String name, int pop, double gdp) { ... }
@Override
public void accept(GeoVisitor visitor) { // 双分派入口
visitor.visitCity(this);
}
// Getters省略
}
// 工业园区实体类
class IndustryElement implements GeoElement {
private String industryType;
private double area;
public IndustryElement(String type, double area) { ... }
@Override
public void accept(GeoVisitor visitor) {
visitor.visitIndustry(this); // 调用工业区专门方法
}
}
// 旅游景区实体类
class SightseeingElement implements GeoElement {
private String landmark;
private int rating;
public SightseeingElement(String name, int stars) { ... }
@Override
public void accept(GeoVisitor visitor) {
visitor.visitSightseeing(this); // 调用景区处理逻辑
}
}
// 客户端使用
public class GeoClient {
public static void main(String[] args) {
// 构建地理数据模型(复杂对象结构)
List<GeoElement> geoGraph = Arrays.asList(
new CityElement("上海", 2487, 43214),
new IndustryElement("化工", 58.3),
new SightseeingElement("外滩", 5)
);
// 应用统计访问者
GeoVisitor exporter = new StatisticsExporter();
geoGraph.forEach(element -> element.accept(exporter)); // 统一访问入口
}
}
核心实现
- 双分派机制:通过element.accept()调用visitor特定方法
- 开放扩展:新增数据操作只需添加访问者类
- 数据结构稳定:元素类无需修改即可支持新业务逻辑
总结

- 访问者(Visitor)接口:声明了一系列以Concrete Element为参数的访问者方法。如果编程语言支持重载,这些方法的名称可以是相同的,但是他们的参数一定是不同的。
- 具体访问者(Concrete Visitor)会为不同的Concrete Element实现相同行为的几个不同版本。
- 元素(Element)接口声明了一个方法来"接收"访问者。这个方法必须有一个参数被声明为访问者接口类型。
- 具体元素(Concrete Element)必须实现接收方法。这个方法的目的是根据当前元素类把调用重定向到相应访问者的方法里。请注意,即使元素基类实现了这个方法,所有子类都必须对它进行重写并调用访问者对象中的合适方法。
- 客户端(Client)通常会作为集合或其他复杂对象(例如一个组合树)的代表。客户端通常不知道所有的具体元素类,因为它们会通过抽象接口与集合中的对象进行交互。