十七、访问者模式


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;
    }

	// 假设 轿车 还有一些别的功能与 吉普车 不同

    @Override
    public void accept(Action action) {
        action.visit(this);
    }
}

2.4 Jeep 类

public class Jeep extends Vehicle { // 吉普车
    public Jeep(String jeepName) {
        this.vehicleName = jeepName;
    }

	// 假设 吉普车 还有一些别的功能与 轿车 不同

    @Override
    public 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 { // 修理行为
    @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 类

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 类

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 类图

alt text
说明: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>() {
                @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 双重派发

访问者模式中的 双重派发 是该模式的一个核心特性,它指的是 当 一个具体访问者对象 访问 一个具体元素对象 时,会根据这两个对象的类型(即 具体访问者类型 和 具体元素类型)来动态地选择并执行相应的方法。这种机制使得 可以在不修改元素类代码的前提下,为元素类添加新的操作

实现方式

  1. 元素类中的 accept() 方法:ConcreteElement 中通常包含一个 accept() 方法,该方法接受一个访问者对象作为参数。当 accept() 方法被调用时,它会 将自身作为参数 传递给访问者对象的某个方法
  2. 访问者类中的操作方法:ConcreteVisitor 中定义了多个操作方法,每个方法对应于一种具体元素。这些方法的具体实现会 根据 具体元素对象的类型 执行相应的操作
  3. 动态方法调用:当元素对象的 accept() 方法被调用时,它会根据具体访问者对象的类型和自身的类型(也就是 ConcreteVisitor 角色 和 ConcreteElement 角色的具体类型),在运行时动态地选择并执行访问者对象中的相应方法。这种 动态方法调用 的过程就是双重派发的实现。

7 优缺点

优点

  • 扩展性好:访问者模式使得 增加新的操作变得容易。当需要给对象结构中的元素添加新的操作时,只需增加一个新的访问者类即可,而无需修改原有的类结构,这符合开闭原则(对扩展开放,对修改关闭)。
  • 灵活性强:访问者模式将 数据结构作用于结构上的操作 解耦,使得 操作 可以相对自由地演化,而不影响 数据结构,这提高了系统的 灵活性
  • 复用性好:访问者模式可以 通过访问者来定义整个对象结构通用的功能,提高了代码的 复用性。特别是当多个访问者共享某些操作时,可以将这些操作提取到访问者接口或父类中,避免代码重复。
  • 符合单一职责原则:访问者模式将相关的操作封装在一起,形成一个访问者类,使得 每个访问者类的职责都比较单一,有助于降低类的复杂度。

缺点

  • 实现复杂:访问者模式的实现 相对复杂,需要定义多个角色和接口,并且需要确保它们之间的正确协作,这无疑会增加系统的复杂性和开发成本。同时,由于访问者模式涉及多个类的交互,因此也增加了系统出错的概率。
  • 难以增加新的具体元素:当需要为对象结构增加新的具体元素时,需要在所有具体访问者类中增加对这个新元素类的操作实现,增加了维护成本。
  • 违反依赖倒置原则:访问者模式在某种程度上违反了依赖倒置原则,因为 具体访问者类 依赖于 具体元素类,而不是依赖于抽象。这可能导致系统耦合度增加,降低系统的可测试性和可维护性。
  • 破坏封装:访问者模式要求 具体元素类 暴露其内部状态给 具体访问者,这可能会破坏元素类的封装性。当元素类的内部状态比较复杂或敏感时,这种破坏可能会带来 安全风险数据一致性 问题。
  • 性能问题:在某些情况下,访问者模式可能会导致性能问题。因为 访问者需要遍历整个对象结构对每个元素执行操作,这可能会增加系统的响应时间或资源消耗。在处理 大型对象结构 时,这种性能问题可能更加明显。

8 适用场景

  • 对象结构复杂且稳定,但操作频繁变化:当系统中的 对象结构 相对复杂且稳定,但经常需要对其中的元素进行多种不同的 操作 时,可以使用访问者模式。这样可以在不修改对象结构的前提下,通过增加新的访问者类来扩展操作。
  • 需要收集操作结果:如果需要对 对象结构 中的元素执行 一系列操作,并需要 收集这些操作的结果进行后续处理 时,可以使用访问者模式。访问者可以在遍历对象结构的过程中,逐步收集操作结果,并在遍历结束后进行统一处理。
  • 设计模式混合使用:在一些复杂的系统中,可能需要将访问者模式与其他设计模式(如组合模式、迭代器模式等)混合使用,以实现更复杂的功能。例如,可以使用 组合模式构建对象结构,然后使用 访问者模式遍历这个结构 并 执行操作
  • 跨平台或跨语言操作:在某些情况下,系统可能需要 与不同的平台或语言进行交互,并 对这些平台或语言中的对象执行操作。使用访问者模式可以将这些操作封装在访问者类中,并通过访问者接口来统一调用,从而简化跨平台或跨语言的操作过程。

9 总结

访问者模式 是一种 行为型 设计模式,它 分离了 数据结构 和 对数据结构的操作,使得能够很容易地添加一种新的操作,遵守了 开闭原则,增强了系统的灵活性和扩展性。但是,这种模式实现起来比较复杂,容易犯错,还难以在数据结构中增加新的具体元素类型,所以在使用前需要慎重考虑。

  • 14
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值