文章目录
- 1 基本介绍
- 2 案例
- 2.1 Element 接口
- 2.2 Vehicle 抽象类
- 2.3 Car 类
- 2.4 Jeep 类
- 2.5 VehicleCollection 类
- 2.6 Action 抽象类
- 2.7 Repair 类
- 2.8 Drive 类
- 2.9 Client 类
- 2.10 Client 类的运行结果
- 2.11 总结
- 3 各角色之间的关系
- 3.1 角色
- 3.1.1 Element ( 元素 )
- 3.1.2 ConcreteElement ( 具体元素 )
- 3.1.3 ObjectStructure ( 对象结构 )
- 3.1.4 Visitor ( 访问者 )
- 3.1.5 ConcreteVisitor ( 具体访问者 )
- 3.1.6 Client ( 客户端 )
- 3.2 类图
- 4 注意事项
- 5 在源码中的使用
- 6 双重派发
- 7 优缺点
- 8 适用场景
- 9 总结
1 基本介绍
访问者模式(Visitor Pattern)是一种 行为型 设计模式,它 将 作用于某种数据结构中的各元素的操作 分离出来封装成独立的类,从而 在不改变数据结构的前提下添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。
本模式据说是最难的一个设计模式,请大家做好心理准备!
2 案例
本案例执行了对一系列车辆(其中含有轿车和吉普车)的各种行为(修理和驾驶),这里 一系列车辆 就相当于 数据结构,各种行为 相当于 对数据结构中各元素的操作。虽然轿车和吉普车的实现差不多,但这里假设它们两个的实现有很大不同,这才需要将其放到两个类中。
2.1 Element 接口
public interface Element { // 接受访问的接口,实现后可以接受 Action 的子类的访问void accept(Action action); // 接受 action 的访问,执行具体的功能
}
2.2 Vehicle 抽象类
public abstract class Vehicle implements Element { // 车辆protected String vehicleName;// 获取车辆名称public String getVehicleName() {return vehicleName;}
}
2.3 Car 类
public class Car extends Vehicle { // 轿车public Car(String carName) {this.vehicleName = carName;}// 假设 轿车 还有一些别的功能与 吉普车 不同@Overridepublic void accept(Action action) {action.visit(this);}
}
2.4 Jeep 类
public class Jeep extends Vehicle { // 吉普车public Jeep(String jeepName) {this.vehicleName = jeepName;}// 假设 吉普车 还有一些别的功能与 轿车 不同@Overridepublic void accept(Action action) {action.visit(this);}
}
2.5 VehicleCollection 类
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 抽象类
public abstract class Action { // 针对具体车辆的行为public abstract void visit(Car car); // 针对 轿车 的行为public abstract void visit(Jeep jeep); // 针对 吉普车 的行为
}
2.7 Repair 类
public class Repair extends Action { // 修理行为@Overridepublic void visit(Car car) {System.out.println("修理轿车[" + car.getVehicleName() + "],使用较小的位置");}@Overridepublic void visit(Jeep jeep) {System.out.println("修理吉普车[" + jeep.getVehicleName() + "],使用较大的位置");}
}
2.8 Drive 类
public class Drive extends Action { // 驾驶行为@Overridepublic void visit(Car car) {System.out.println("驾驶轿车[" + car.getVehicleName() + "],在城市的公路上行驶");}@Overridepublic void visit(Jeep jeep) {System.out.println("驾驶吉普车[" + jeep.getVehicleName() + "],在凹凸不平的路上行驶");}
}
2.9 Client 类
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
接口,它包含一组方法,在遍历文件系统时会被调用:public 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
实现,它遍历一个目录树,并打印出所有文件的名称:import 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>() {@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs)throws IOException {System.out.println(file);return FileVisitResult.CONTINUE;}@Overridepublic 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 总结
访问者模式 是一种 行为型 设计模式,它 分离了 数据结构 和 对数据结构的操作,使得能够很容易地添加一种新的操作,遵守了 开闭原则,增强了系统的灵活性和扩展性。但是,这种模式实现起来比较复杂,容易犯错,还难以在数据结构中增加新的具体元素类型,所以在使用前需要慎重考虑。