访问者模式 (Visitor Pattern)

定义

访问者模式(Visitor Pattern)是一种行为型设计模式,用于将算法与其作用于的对象结构分离。这种模式主要用于执行操作或应用过程,这些操作需要在不同类型的对象上执行,同时避免让这些对象的类变得过于复杂。

关键组成部分

  1. 访问者(Visitor)
    • 一个接口或抽象类,定义了对不同类型元素(Element)的访问操作。
    • 实现了每种类型元素的操作,是将操作逻辑从元素类中分离出来的关键所在。
  2. 元素(Element)
    • 定义了一个接受访问者的方法(通常为 accept),该方法允许访问者对象访问元素。
    • 元素结构通常稳定,且含有多个接受访问的方法,每个方法对应一种类型的访问者。
  3. 具体元素(Concrete Element)
    • 实现元素接口,定义了 accept 方法的具体实现。
    • 可能有多个不同类型的具体元素类,每个类都有自己的逻辑和接受访问者的方式。
  4. 具体访问者(Concrete Visitor)
    • 实现访问者接口,定义了对每个元素类的具体操作。
    • 可能有多个不同类型的具体访问者,每个访问者都实现了一套作用于元素的操作。
解决的问题
  • 操作与对象结构的分离:在复杂对象结构中,经常需要执行各种不依赖于特定对象的操作。访问者模式使得可以将这些操作从对象结构中分离出来,以减少这些操作对于对象结构的影响。
  • 添加新操作的灵活性:当新的操作需要在这些对象上执行时,你可能不希望更改这些对象的类。访问者模式允许你通过添加新的访问者类来添加新的操作,而无需修改对象的类。
  • 集中相关操作:在传统的面向对象设计中,相关的操作可能分散在各个类中。访问者模式允许你将相关操作集中在一个访问者类中,这样可以避免在对象结构中散布这些操作,从而提高代码的组织性和可维护性。
  • 扩展性:对于那些可能需要添加新操作的对象结构,访问者模式提供了一种容易扩展的方式。你可以在不更改现有代码的情况下,通过创建新的访问者来添加新的操作。
  • 聚合操作:在一些情况下,你可能需要对一个复杂的对象结构执行聚合操作,如遍历、搜索或生成报告。访问者模式使得这些操作可以被集中管理和维护。
使用场景
  • 复杂对象结构:在复杂的对象结构中(如树状或图状结构),需要对结构中的各个对象执行操作,而这些操作依赖于对象的具体类型。访问者模式允许在不修改这些对象类的情况下,添加新的操作。
  • 添加新操作:当需要对一个对象结构添加新的操作,且不希望这些操作影响到对象的类时。访问者模式允许将操作逻辑封装在访问者中,易于扩展。
  • 避免"污染"对象类:如果在每个对象类中添加新操作会导致类变得复杂或不易维护,那么使用访问者模式将这些操作外部化是一个好选择。
  • 不同的访问者实现不同的操作:当同一个对象结构需要支持多种不同的操作,且这些操作是互相独立的。例如,可能有一个用于渲染对象的访问者,另一个用于检查对象的完整性。
  • 频繁变更的操作:如果一组操作经常变更,但对象结构相对稳定,那么将这些操作作为访问者的一部分,可以避免频繁修改对象结构。
  • 累积状态:在遍历一个复杂结构时,如果需要在访问者中累积状态,而不是在元素中累积,那么访问者模式也是一个不错的选择。
示例代码1-计算机部件访问者

在这个例子中,我们定义了一个计算机部件(ComputerPart)的接口和一些具体部件类(Keyboard、Monitor、Mouse),以及一个访问者接口(ComputerPartVisitor)和一个具体的访问者实现(ComputerPartDisplayVisitor)。

// 访问者接口
interface ComputerPartVisitor {
    void visit(Computer computer);
    void visit(Mouse mouse);
    void visit(Keyboard keyboard);
    void visit(Monitor monitor);
}

// 元素接口
interface ComputerPart {
    void accept(ComputerPartVisitor computerPartVisitor);
}

// 元素实现
class Keyboard implements ComputerPart {
    public void accept(ComputerPartVisitor computerPartVisitor) {
        computerPartVisitor.visit(this);
    }
}

class Monitor implements ComputerPart {
    public void accept(ComputerPartVisitor computerPartVisitor) {
        computerPartVisitor.visit(this);
    }
}

class Mouse implements ComputerPart {
    public void accept(ComputerPartVisitor computerPartVisitor) {
        computerPartVisitor.visit(this);
    }
}

class Computer implements ComputerPart {
    ComputerPart[] parts;

    public Computer(){
        parts = new ComputerPart[] {new Mouse(), new Keyboard(), new Monitor()};		
    }

    public void accept(ComputerPartVisitor computerPartVisitor) {
        for (int i = 0; i < parts.length; i++) {
            parts[i].accept(computerPartVisitor);
        }
        computerPartVisitor.visit(this);
    }
}

// 具体访问者
class ComputerPartDisplayVisitor implements ComputerPartVisitor {
    public void visit(Computer computer) {
        System.out.println("Displaying Computer.");
    }

    public void visit(Mouse mouse) {
        System.out.println("Displaying Mouse.");
    }

    public void visit(Keyboard keyboard) {
        System.out.println("Displaying Keyboard.");
    }

    public void visit(Monitor monitor) {
        System.out.println("Displaying Monitor.");
    }
}

// 客户端
public class VisitorPatternDemo {
    public static void main(String[] args) {
        ComputerPart computer = new Computer();
        computer.accept(new ComputerPartDisplayVisitor());
    }
}
示例代码2-媒体文件的操作

在这个例子中,我们有不同类型的媒体文件(如音频文件和视频文件),并希望执行不同的操作,例如播放和编码。我们将定义媒体文件的接口和具体类,以及一个访问者接口和两个具体的访问者实现。

// 媒体文件接口
interface MediaFile {
    void accept(MediaFileVisitor visitor);
}

// 音频文件
class AudioFile implements MediaFile {
    private String filename;

    public AudioFile(String filename) {
        this.filename = filename;
    }

    public String getFilename() {
        return filename;
    }

    @Override
    public void accept(MediaFileVisitor visitor) {
        visitor.visit(this);
    }
}

// 视频文件
class VideoFile implements MediaFile {
    private String filename;

    public VideoFile(String filename) {
        this.filename = filename;
    }

    public String getFilename() {
        return filename;
    }

    @Override
    public void accept(MediaFileVisitor visitor) {
        visitor.visit(this);
    }
}


// 媒体文件访问者接口
interface MediaFileVisitor {
    void visit(AudioFile audio);
    void visit(VideoFile video);
}

// 播放操作访问者
class PlayVisitor implements MediaFileVisitor {
    @Override
    public void visit(AudioFile audio) {
        System.out.println("Playing audio file: " + audio.getFilename());
    }

    @Override
    public void visit(VideoFile video) {
        System.out.println("Playing video file: " + video.getFilename());
    }
}

// 编码操作访问者
class EncodeVisitor implements MediaFileVisitor {
    @Override
    public void visit(AudioFile audio) {
        System.out.println("Encoding audio file: " + audio.getFilename());
    }

    @Override
    public void visit(VideoFile video) {
        System.out.println("Encoding video file: " + video.getFilename());
    }
}

public class VisitorDemo {
    public static void main(String[] args) {
        MediaFile audio = new AudioFile("song.mp3");
        MediaFile video = new VideoFile("movie.mp4");

        MediaFileVisitor playVisitor = new PlayVisitor();
        MediaFileVisitor encodeVisitor = new EncodeVisitor();

        audio.accept(playVisitor);
        video.accept(playVisitor);

        audio.accept(encodeVisitor);
        video.accept(encodeVisitor);
    }
}

在这个例子中,MediaFile 接口定义了接受访问者的方法。AudioFileVideoFile 是具体的媒体文件类。MediaFileVisitor 接口定义了访问者的行为,而 PlayVisitorEncodeVisitor 是具体的访问者实现,它们实现了对不同媒体文件进行播放和编码的操作。这样,当需要为媒体文件添加新的操作时,我们只需要添加新的访问者,而不必修改媒体文件类。

主要符合的设计原则
  • 开闭原则(Open-Closed Principle)
    • 访问者模式允许在不修改现有代码的情况下引入新的操作。这意味着类可以保持开放以供扩展(通过添加新的访问者来实现新的功能),但对修改是关闭的(因为你不需要改变现有的类和对象结构)。
  • 单一职责原则(Single Responsibility Principle)
    • 在访问者模式中,元素类的职责是维护其核心功能和数据,而访问者类的职责是执行在这些元素上的特定操作。这种分离确保了单一职责原则,即每个类或模块只有一个原因导致改变。
  • 依赖倒置原则(Dependency Inversion Principle)
    • 访问者模式通常定义了抽象访问者和抽象元素接口,具体的访问者和元素类都依赖于这些接口,而不是具体的实现。这符合依赖倒置原则,即高层模块不应该依赖于低层模块的具体实现,而应该依赖于抽象。
  • 里氏替换原则(Liskov Substitution Principle)
    • 在访问者模式中,可以用子类的对象替换父类的对象,而程序的行为不会发生变化。比如在访问者模式中,可以用具体元素的子类来替换父类元素,而访问者的行为不会改变。
在JDK中的应用
  • Java文件I/O(NIO)
    • java.nio.file.FileVisitor 接口是访问者模式的一个经典应用。它用于遍历文件系统的目录树。FileVisitor 接口定义了在访问目录树的过程中可以执行的一系列操作(如访问文件前后的操作),而具体的行为则由实现了 FileVisitor 接口的类定义。
    • SimpleFileVisitor 类是 FileVisitor 的一个实现,它提供了对遍历过程中各种事件的基本处理。
  • Java编译API
    • javax.lang.model 包中,Java编译API使用了访问者模式来处理抽象语法树(AST)。这个API允许开发者在编译时检查、处理和生成Java代码。
    • ElementElementVisitor 接口及其相关类在这个包中用于表示和访问AST中的元素。
  • Java反射API
    • java.lang.reflect 包中,反射API提供了一种访问者风格的接口,用于检查类和对象的运行时行为。例如,Visitor 模式用于在不同类型的 AnnotatedElement(如类、方法、字段等)上执行操作。
在Spring中的应用
  • Spring框架中没有直接的访问者模式实例,但是框架的设计允许并鼓励使用访问者模式来实现跨多个类的操作和维护。