0%

访问者模式

访问者模式(Visitor Pattern):数据与操作的分离艺术

访问者模式是行为型设计模式的一种,核心思想是封装作用于数据结构中各元素的操作,使这些操作可以独立于数据结构变化。它通过 “双分派” 机制,在不修改元素类的前提下,为元素添加新的操作,本质是 “预留访问通路,通过回调实现操作扩展”。

访问者模式的核心结构

访问者模式

访问者模式通过五个核心角色实现数据与操作的分离,分工明确且支持灵活扩展:

抽象元素(Element)

  • 定义数据结构中元素的抽象接口,声明一个accept(Visitor visitor)方法,用于接受访问者的访问(核心方法,实现双分派的关键)。
  • 示例:Shape(图形抽象类,声明accept(Visitor v))。

具体元素(ConcreteElement)

  • 实现抽象元素接口,重写accept方法,并在方法中调用访问者的对应visit方法(将自身作为参数传递)。
  • 示例:Circle(圆形)、Rectangle(矩形)。

抽象访问者(Visitor)

  • 定义对所有具体元素的访问接口,每个方法对应一种具体元素(方法名通常为visitConcreteElementX)。
  • 示例:ShapeVisitor(图形访问者接口,声明visitCircle(Circle c)visitRectangle(Rectangle r))。

具体访问者(ConcreteVisitor)

  • 实现抽象访问者接口,封装对具体元素的操作逻辑(如计算面积、绘制图形等)。
  • 示例:AreaCalculator(计算面积的访问者)、ShapeDrawer(绘制图形的访问者)。

5对象结构(ObjectStructure)

  • 管理元素的集合(如列表、树等),提供遍历元素的方法,并可触发访问者对所有元素的访问。
  • 示例:ShapeGroup(图形组,包含多个图形元素)。

核心机制:双分派(Double Dispatch)

访问者模式的关键是 “双分派” 机制,通过两次分发决定最终执行的操作:

  1. 第一次分派:客户端调用元素的accept(visitor)方法(元素类型确定)。
  2. 第二次分派:元素的accept方法调用访问者的visitConcreteElement(this)方法(访问者类型确定)。

这种机制确保了 “操作” 与 “元素” 的动态绑定,使新增操作无需修改元素类。

代码实现示例

以 “图形操作” 为例:存在圆形、矩形等图形(元素),需要计算面积、绘制图形等操作(访问者),通过访问者模式使操作与图形分离。

1. 抽象元素与具体元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 1. 抽象元素:图形
public abstract class Shape {
// 接受访问者访问
public abstract void accept(ShapeVisitor visitor);
}

// 2. 具体元素:圆形
public class Circle extends Shape {
private double radius; // 半径

public Circle(double radius) {
this.radius = radius;
}

public double getRadius() {
return radius;
}

@Override
public void accept(ShapeVisitor visitor) {
// 调用访问者的对应方法(第二次分派)
visitor.visitCircle(this);
}
}

// 3. 具体元素:矩形
public class Rectangle extends Shape {
private double width; // 宽
private double height; // 高

public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}

public double getWidth() { return width; }
public double getHeight() { return height; }

@Override
public void accept(ShapeVisitor visitor) {
// 调用访问者的对应方法(第二次分派)
visitor.visitRectangle(this);
}
}

2. 抽象访问者与具体访问者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 4. 抽象访问者:图形访问者
public interface ShapeVisitor {
// 访问圆形
void visitCircle(Circle circle);
// 访问矩形
void visitRectangle(Rectangle rectangle);
}

// 5. 具体访问者1:计算面积
public class AreaCalculator implements ShapeVisitor {
private double totalArea; // 总面积

@Override
public void visitCircle(Circle circle) {
double area = Math.PI * circle.getRadius() * circle.getRadius();
totalArea += area;
System.out.printf("圆形面积:%.2f%n", area);
}

@Override
public void visitRectangle(Rectangle rectangle) {
double area = rectangle.getWidth() * rectangle.getHeight();
totalArea += area;
System.out.printf("矩形面积:%.2f%n", area);
}

public double getTotalArea() {
return totalArea;
}
}

// 6. 具体访问者2:绘制图形
public class ShapeDrawer implements ShapeVisitor {
@Override
public void visitCircle(Circle circle) {
System.out.printf("绘制圆形(半径:%.2f)%n", circle.getRadius());
}

@Override
public void visitRectangle(Rectangle rectangle) {
System.out.printf("绘制矩形(宽:%.2f,高:%.2f)%n", rectangle.getWidth(), rectangle.getHeight());
}
}

3. 对象结构与客户端使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 7. 对象结构:图形组
public class ShapeGroup {
private List<Shape> shapes = new ArrayList<>();

public void addShape(Shape shape) {
shapes.add(shape);
}

// 让访问者访问所有图形
public void accept(ShapeVisitor visitor) {
for (Shape shape : shapes) {
shape.accept(visitor); // 触发第一次分派
}
}
}

// 8. 客户端
public class VisitorDemo {
public static void main(String[] args) {
// 创建对象结构(图形组)
ShapeGroup group = new ShapeGroup();
group.addShape(new Circle(2)); // 半径2的圆
group.addShape(new Rectangle(3, 4)); // 3x4的矩形

// 操作1:计算面积(使用AreaCalculator访问者)
AreaCalculator areaVisitor = new AreaCalculator();
group.accept(areaVisitor);
System.out.printf("总面积:%.2f%n", areaVisitor.getTotalArea());

// 操作2:绘制图形(使用ShapeDrawer访问者)
ShapeDrawer drawVisitor = new ShapeDrawer();
group.accept(drawVisitor);
}
}
输出结果
1
2
3
4
5
圆形面积:12.57
矩形面积:12.00
总面积:24.57
绘制圆形(半径:2.00)
绘制矩形(宽:3.00,高:4.00)

访问者模式的核心优势

  1. 数据与操作分离
    元素类(如Circle)仅关注自身数据,操作逻辑(如计算面积)封装在访问者中,符合单一职责原则。
  2. 灵活扩展新操作
    新增操作只需添加新的具体访问者(如新增 “计算周长” 的PerimeterCalculator),无需修改元素类,符合开闭原则。
  3. 集中管理相关操作
    同一类操作(如所有与面积相关的计算)可集中在一个访问者中,便于维护(如修改面积计算公式只需改一个类)。
  4. 遍历复杂结构
    对象结构可提供统一遍历接口,访问者可对复杂数据结构(如树、图)中的所有元素执行操作,无需关心结构细节。

适用场景

  1. 数据结构稳定,但操作多变
    当元素类(如图形、员工信息)很少变动,但需要频繁添加新操作(如统计、导出、展示)时(如报表系统:数据模型固定,需生成多种报表)。
  2. 操作依赖于元素的具体类型
    若操作逻辑因元素类型不同而差异较大(如圆形和矩形的面积计算方式完全不同),访问者模式可避免在元素类中使用大量if-else判断。
  3. 需要对复杂对象结构进行多维度操作
    如对公司组织架构(包含部门、员工等元素)进行 “薪资统计”“年龄分析”“岗位分布” 等多维度分析,每个维度对应一个访问者。

优缺点分析

优点

  • 高扩展性:新增操作只需新增访问者,无需修改元素或对象结构。
  • 职责清晰:数据(元素)与行为(访问者)分离,各自专注于自身职责。
  • 集中化操作:相关操作集中在访问者中,便于代码复用和维护。

缺点

  • 元素类型扩展困难:若新增元素类型(如新增Triangle),所有访问者接口及其实现都需修改(违反开闭原则)。
  • 破坏封装性:访问者需访问元素的内部状态(如Circle的半径),可能暴露元素的私有数据。
  • 学习成本高:双分派机制较难理解,代码逻辑间接性强(元素调用访问者,访问者再调用元素)。

经典应用案例

  1. 编译器的语法树分析
    语法树节点(元素)固定(如表达式、语句),但需要进行语义分析、代码生成、优化等多种操作(访问者),访问者模式可分离这些操作。
  2. XML/JSON 文档解析
    文档节点(元素)结构固定,需进行校验、转换、提取数据等操作(访问者),通过访问者统一处理不同节点。
  3. 集合框架的迭代与操作
    如 Java 的FileVisitor(遍历文件系统时,对文件和目录执行不同操作:删除、复制、统计等)。
  4. 报表生成系统
    数据模型(元素)固定,需生成表格、图表、PDF 等不同格式报表(访问者),新增报表类型只需添加新访问者。

总结

访问者模式通过 “双分派” 机制实现了数据与操作的解耦,特别适合 “数据结构稳定但操作多变” 的场景。其核心价值在于将分散在元素类中的操作集中管理,支持灵活扩展新行为。但需注意,若元素类型频繁变化,访问者模式会导致大量代码修改,此时不宜使用

欢迎关注我的其它发布渠道