文章目录
- [1 基本介绍](#1 基本介绍)
- [2 案例](#2 案例)
-
- [2.1 Element 接口](#2.1 Element 接口)
- [2.2 Vehicle 抽象类](#2.2 Vehicle 抽象类)
- [2.3 Car 类](#2.3 Car 类)
- [2.4 Jeep 类](#2.4 Jeep 类)
- [2.5 VehicleCollection 类](#2.5 VehicleCollection 类)
- [2.6 Action 抽象类](#2.6 Action 抽象类)
- [2.7 Repair 类](#2.7 Repair 类)
- [2.8 Drive 类](#2.8 Drive 类)
- [2.9 Client 类](#2.9 Client 类)
- [2.10 Client 类的运行结果](#2.10 Client 类的运行结果)
- [2.11 总结](#2.11 总结)
- [3 各角色之间的关系](#3 各角色之间的关系)
-
- [3.1 角色](#3.1 角色)
-
- [3.1.1 Element ( 元素 )](#3.1.1 Element ( 元素 ))
- [3.1.2 ConcreteElement ( 具体元素 )](#3.1.2 ConcreteElement ( 具体元素 ))
- [3.1.3 ObjectStructure ( 对象结构 )](#3.1.3 ObjectStructure ( 对象结构 ))
- [3.1.4 Visitor ( 访问者 )](#3.1.4 Visitor ( 访问者 ))
- [3.1.5 ConcreteVisitor ( 具体访问者 )](#3.1.5 ConcreteVisitor ( 具体访问者 ))
- [3.1.6 Client ( 客户端 )](#3.1.6 Client ( 客户端 ))
- [3.2 类图](#3.2 类图)
- [4 注意事项](#4 注意事项)
- [5 在源码中的使用](#5 在源码中的使用)
- [6 双重派发](#6 双重派发)
- [7 优缺点](#7 优缺点)
- [8 适用场景](#8 适用场景)
- [9 总结](#9 总结)
1 基本介绍
访问者模式 (Visitor Pattern)是一种 行为型 设计模式,它 将 作用于某种数据结构中的各元素的操作 分离出来封装成独立的类 ,从而 在不改变数据结构的前提下添加作用于这些元素的新的操作 ,为数据结构中的每个元素提供多种访问方式。
本模式据说是最难的一个设计模式,请大家做好心理准备!
2 案例
本案例执行了对一系列车辆(其中含有轿车和吉普车)的各种行为(修理和驾驶),这里 一系列车辆 就相当于 数据结构 ,各种行为 相当于 对数据结构中各元素的操作。虽然轿车和吉普车的实现差不多,但这里假设它们两个的实现有很大不同,这才需要将其放到两个类中。
2.1 Element 接口
java
public interface Element { // 接受访问的接口,实现后可以接受 Action 的子类的访问
void accept(Action action); // 接受 action 的访问,执行具体的功能
}
2.2 Vehicle 抽象类
java
public abstract class Vehicle implements Element { // 车辆
protected String vehicleName;
// 获取车辆名称
public String getVehicleName() {
return vehicleName;
}
}
2.3 Car 类
java
public class Car extends Vehicle { // 轿车
public Car(String carName) {
this.vehicleName = carName;
}
// 假设 轿车 还有一些别的功能与 吉普车 不同
@Override
public void accept(Action action) {
action.visit(this);
}
}
2.4 Jeep 类
java
public class Jeep extends Vehicle { // 吉普车
public Jeep(String jeepName) {
this.vehicleName = jeepName;
}
// 假设 吉普车 还有一些别的功能与 轿车 不同
@Override
public void accept(Action action) {
action.visit(this);
}
}
2.5 VehicleCollection 类
java
import java.util.ArrayList;
import java.util.List;
public class VehicleCollection { // 车辆集合
private List<Vehicle> vehicles = new ArrayList<>(); // 储存车辆的集合
// 添加一个新的车辆到本集合中
public void addVehicle(Vehicle vehicle) {
vehicles.add(vehicle);
}
// 让集合中的所有车辆都进行一遍指定的 action 行为
public void forEach(Action action) {
for (Vehicle vehicle : vehicles) {
vehicle.accept(action);
}
}
}
2.6 Action 抽象类
java
public abstract class Action { // 针对具体车辆的行为
public abstract void visit(Car car); // 针对 轿车 的行为
public abstract void visit(Jeep jeep); // 针对 吉普车 的行为
}
2.7 Repair 类
java
public class Repair extends Action { // 修理行为
@Override
public void visit(Car car) {
System.out.println("修理轿车[" + car.getVehicleName() + "],使用较小的位置");
}
@Override
public void visit(Jeep jeep) {
System.out.println("修理吉普车[" + jeep.getVehicleName() + "],使用较大的位置");
}
}
2.8 Drive 类
java
public class Drive extends Action { // 驾驶行为
@Override
public void visit(Car car) {
System.out.println("驾驶轿车[" + car.getVehicleName() + "],在城市的公路上行驶");
}
@Override
public void visit(Jeep jeep) {
System.out.println("驾驶吉普车[" + jeep.getVehicleName() + "],在凹凸不平的路上行驶");
}
}
2.9 Client 类
java
public class Client { // 客户端,测试了对一系列车辆的修理和驾驶行为
public static void main(String[] args) {
Action repair = new Repair(); // 修理行为
Action drive = new Drive(); // 驾驶行为
VehicleCollection vehicleCollection = new VehicleCollection();
vehicleCollection.addVehicle(new Car("本田")); // 轿车
vehicleCollection.addVehicle(new Jeep("牧马人")); // 吉普车
// 进行修理行为
vehicleCollection.forEach(repair);
System.out.println("===============================");
// 进行驾驶行为
vehicleCollection.forEach(drive);
}
}
2.10 Client 类的运行结果
修理轿车[本田],使用较小的位置
修理吉普车[牧马人],使用较大的位置
===============================
驾驶轿车[本田],在城市的公路上行驶
驾驶吉普车[牧马人],在凹凸不平的路上行驶
2.11 总结
本案例将 一系列车辆 (其中含有轿车和吉普车)看作 数据结构 ,将 对单个车辆的行为 (修理和驾驶)看作 对数据结构的访问 ,使用访问者模式将数据结构与对其的访问分隔开来,从而在不用修改原有代码的情况下,能够添加新的访问形式(添加新的对单个车辆的行为,例如购买),遵循了 开闭原则,提高了系统的灵活性和扩展性。
但是,如果想要添加一种新的数据结构(添加一种新的车辆,例如货车),则比较麻烦。需要在 Action
类中添加一个新的访问方法 public abstract void visit(? ?)
,这里的 ?
指的是添加的具体的数据结构的类型及其参数名。此外,还需要给现有的所有继承 Action
类的类都实现这个方法。
3 各角色之间的关系
3.1 角色
3.1.1 Element ( 元素 )
该角色是 Visitor 角色的访问对象 ,声明了接受访问的 accept()
方法 ,接收 Visitor 角色的参数 。本案例中,Element
接口扮演了该角色。
3.1.2 ConcreteElement ( 具体元素 )
该角色负责 实现 Element 角色定义的接口 。本案例中,Car, Jeep
类都在扮演该角色。
3.1.3 ObjectStructure ( 对象结构 )
该角色是 处理 Element 角色的集合 ,有一个对集合中所有元素进行指定操作的方法 。本案例中,VehicleCollection
类扮演了该角色。
3.1.4 Visitor ( 访问者 )
该角色负责 为 ObjectStructure 角色中的每个 ConcreteElement 角色定义 visit() 接口 。本案例中,Action
抽象类扮演了该角色。
3.1.5 ConcreteVisitor ( 具体访问者 )
该角色负责 实现 Visitor 角色中定义的 接口 ,具体处理每个 ConcreteElement 角色 。本案例中,Repair, Drive
类都在扮演该角色。
3.1.6 Client ( 客户端 )
该角色负责 创建 ConcreteElement 角色和 ConcreteVisitor 角色 ,使用 ObjectStructure 角色完成具体的业务逻辑 。本案例中,Client
类扮演了该角色。
3.2 类图
说明:ConcreteVisitor 和 ConcreteElement 实际上是相互依赖的,为了避免关系过于复杂,图中没有表示。
4 注意事项
- 设计复杂性:访问者模式需要定义多个角色(如访问者、元素、结构对象等)和接口,以及确保它们之间的正确协作,这会增加系统的复杂性和开发成本。当对象结构发生变化时,可能需要在多个访问者类中更新代码,这增加了维护的难度和成本。
- 性能问题 :访问者模式需要 遍历整个对象结构 ,对每个元素执行操作 ,这可能会增加系统的响应时间或资源消耗。在处理 大型对象结构 时,这种性能问题可能更加明显。
- 新元素类的添加 :虽然访问者模式允许在不修改原有类结构的情况下增加 新的操作 ,但增加 新的元素类 时,需要在所有具体访问者类中增加对新元素类的操作实现。
- 封装性破坏 :访问者模式要求元素类暴露其内部状态给访问者,这可能会 破坏元素类的封装性。当元素类的内部状态比较复杂或敏感时,这种破坏可能会带来安全风险或数据一致性问题。
- 单一职责原则:虽然访问者模式有助于遵守单一职责原则,但在实现时需要注意不要过度使用,以免增加系统的复杂性和维护成本。
- 依赖倒转原则 :尽量 让 访问者 依赖于 抽象类 而不是 具体类,以符合依赖倒转原则,降低系统间的耦合度。
5 在源码中的使用
在 java.nio.file
包中,使用 FileVisitor
接口时应用了访问者模式,其对应的角色如下:
-
ConcreteElement 角色 :
Path
类可以被视为是访问者模式中的 ConcreteElement 角色,因为它是被访问的对象。注意,在FileVisitor
使用的访问者模式中,没有直接定义接口或抽象类来表示 Element 角色。 -
ObjectStructure 角色 :文件系统本身 就是这个对象结构,
Files.walkFileTree()
方法则是这个对象结构的遍历器,它接受一个起始目录和一个FileVisitor
实例,然后遍历该目录及其子目录中的所有文件和目录。 -
Visitor 角色 :
FileVisitor
接口,它包含一组方法,在遍历文件系统时会被调用:javapublic interface FileVisitor<T> { // 在访问目录之前被调用 FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException; // 访问文件时被调用 FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException; // 访问文件失败时被调用 FileVisitResult visitFileFailed(T file, IOException exc) throws IOException; // 在访问目录之后被调用 FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException; }
-
ConcreteVisitor 角色 :通过实现
FileVisitor
接口来创建自己的具体访问者,定义在访问文件或目录时应该执行的具体逻辑 。例如,以下是一个简单的FileVisitor
实现,它遍历一个目录树,并打印出所有文件的名称:javaimport java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; public class Test { public static void main(String[] args) throws IOException { Path start = Paths.get("/dir"); // 指定具体的目录 Files.walkFileTree(start, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { System.out.println(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { return FileVisitResult.CONTINUE; } // 可以根据需要重写其他方法 }); } }
使用访问者模式,就可以通过实现 FileVisitor
接口来对文件系统(相当于一个数据结构)进行特定的操作,这样遵循了 开闭原则 ,使得 FileVisitor
接口和 Files.walkFileTree()
方法能够重复使用,即具有 复用性。
6 双重派发
访问者模式中的 双重派发 是该模式的一个核心特性,它指的是 当 一个具体访问者对象 访问 一个具体元素对象 时,会根据这两个对象的类型(即 具体访问者类型 和 具体元素类型)来动态地选择并执行相应的方法 。这种机制使得 可以在不修改元素类代码的前提下,为元素类添加新的操作。
实现方式:
- 元素类中的
accept()
方法 :ConcreteElement 中通常包含一个accept()
方法,该方法接受一个访问者对象作为参数。当accept()
方法被调用时,它会 将自身作为参数 传递给访问者对象的某个方法。 - 访问者类中的操作方法 :ConcreteVisitor 中定义了多个操作方法,每个方法对应于一种具体元素。这些方法的具体实现会 根据 具体元素对象的类型 执行相应的操作。
- 动态方法调用 :当元素对象的
accept()
方法被调用时,它会根据具体访问者对象的类型和自身的类型(也就是 ConcreteVisitor 角色 和 ConcreteElement 角色的具体类型 ),在运行时动态地选择并执行访问者对象中的相应方法 。这种 动态方法调用 的过程就是双重派发的实现。
7 优缺点
优点:
- 扩展性好 :访问者模式使得 增加新的操作变得容易。当需要给对象结构中的元素添加新的操作时,只需增加一个新的访问者类即可,而无需修改原有的类结构,这符合开闭原则(对扩展开放,对修改关闭)。
- 灵活性强 :访问者模式将 数据结构 与 作用于结构上的操作 解耦,使得 操作 可以相对自由地演化,而不影响 数据结构,这提高了系统的 灵活性。
- 复用性好 :访问者模式可以 通过访问者来定义整个对象结构通用的功能 ,提高了代码的 复用性。特别是当多个访问者共享某些操作时,可以将这些操作提取到访问者接口或父类中,避免代码重复。
- 符合单一职责原则 :访问者模式将相关的操作封装在一起,形成一个访问者类,使得 每个访问者类的职责都比较单一,有助于降低类的复杂度。
缺点:
- 实现复杂 :访问者模式的实现 相对复杂,需要定义多个角色和接口,并且需要确保它们之间的正确协作,这无疑会增加系统的复杂性和开发成本。同时,由于访问者模式涉及多个类的交互,因此也增加了系统出错的概率。
- 难以增加新的具体元素:当需要为对象结构增加新的具体元素时,需要在所有具体访问者类中增加对这个新元素类的操作实现,增加了维护成本。
- 违反依赖倒置原则 :访问者模式在某种程度上违反了依赖倒置原则,因为 具体访问者类 依赖于 具体元素类,而不是依赖于抽象。这可能导致系统耦合度增加,降低系统的可测试性和可维护性。
- 破坏封装 :访问者模式要求 具体元素类 暴露其内部状态给 具体访问者 ,这可能会破坏元素类的封装性。当元素类的内部状态比较复杂或敏感时,这种破坏可能会带来 安全风险 或 数据一致性 问题。
- 性能问题 :在某些情况下,访问者模式可能会导致性能问题。因为 访问者需要遍历整个对象结构 ,对每个元素执行操作 ,这可能会增加系统的响应时间或资源消耗。在处理 大型对象结构 时,这种性能问题可能更加明显。
8 适用场景
- 对象结构复杂且稳定,但操作频繁变化 :当系统中的 对象结构 相对复杂且稳定,但经常需要对其中的元素进行多种不同的 操作 时,可以使用访问者模式。这样可以在不修改对象结构的前提下,通过增加新的访问者类来扩展操作。
- 需要收集操作结果 :如果需要对 对象结构 中的元素执行 一系列操作 ,并需要 收集这些操作的结果进行后续处理 时,可以使用访问者模式。访问者可以在遍历对象结构的过程中,逐步收集操作结果,并在遍历结束后进行统一处理。
- 设计模式混合使用 :在一些复杂的系统中,可能需要将访问者模式与其他设计模式(如组合模式、迭代器模式等)混合使用 ,以实现更复杂的功能。例如,可以使用 组合模式 来 构建对象结构 ,然后使用 访问者模式 来 遍历这个结构 并 执行操作。
- 跨平台或跨语言操作 :在某些情况下,系统可能需要 与不同的平台或语言进行交互 ,并 对这些平台或语言中的对象执行操作。使用访问者模式可以将这些操作封装在访问者类中,并通过访问者接口来统一调用,从而简化跨平台或跨语言的操作过程。
9 总结
访问者模式 是一种 行为型 设计模式,它 分离了 数据结构 和 对数据结构的操作 ,使得能够很容易地添加一种新的操作,遵守了 开闭原则,增强了系统的灵活性和扩展性。但是,这种模式实现起来比较复杂,容易犯错,还难以在数据结构中增加新的具体元素类型,所以在使用前需要慎重考虑。