文章目录
- 前言
- 一、概念
- 二、核心结构
- [三、Java 代码实现(文件系统:文件+文件夹)](#三、Java 代码实现(文件系统:文件+文件夹))
-
- [1. 抽象元素(FileSystemElement)](#1. 抽象元素(FileSystemElement))
- [2. 具体元素:文件 File](#2. 具体元素:文件 File)
- [3. 具体元素:文件夹 Folder](#3. 具体元素:文件夹 Folder)
- [4. 抽象访问者 Visitor](#4. 抽象访问者 Visitor)
- [5. 具体访问者1:打印功能](#5. 具体访问者1:打印功能)
- [6. 具体访问者2:计算总大小](#6. 具体访问者2:计算总大小)
- [7. 客户端](#7. 客户端)
- 四、双分派(核心机制)
- 五、优缺点
- 六、应用场景
- [七、访问者 VS 迭代器 VS 组合](#七、访问者 VS 迭代器 VS 组合)
- 八、总结
前言
在开发中,我们经常遇到稳定的数据结构 + 多变的操作逻辑 场景:比如一个树形结构(文件、订单、商品),需要新增打印、压缩、审计、报表 等不同功能。如果每次加功能都去修改元素类,会严重违背开闭原则。访问者模式 就是专门解决稳定结构、多变操作 的行为型设计模式,让你不修改元素,就能无限扩展新功能。
一、概念
访问者模式(Visitor Pattern) 是一种行为型设计模式 ,核心思想:
封装作用于某种数据结构(如List、Set、Map、组合树)中各元素的操作,在不改变元素类的前提下,定义作用于这些元素的新操作。
简单理解:
- 数据结构(元素)稳定不变
- 功能(访问者)可以无限扩展
- 把操作从元素类中抽离出来,交给访问者
- 双分派:元素接受访问,访问者处理元素
一句话总结:
结构不动,功能扩展;数据归你,操作归我。
二、核心结构
- Visitor(抽象访问者)
为每类元素声明一个访问方法。 - ConcreteVisitor(具体访问者)
实现具体操作逻辑(打印、报表、压缩等)。 - Element(抽象元素)
提供accept(Visitor)方法,接受访问。 - ConcreteElement(具体元素)
实现accept,允许访问者访问自己。 - ObjectStructure(对象结构)
持有元素集合,可遍历让访问者访问。
三、Java 代码实现(文件系统:文件+文件夹)
结构:文件夹(组合)+ 文件(叶子)
功能:打印、提取大小(两个访问者)
1. 抽象元素(FileSystemElement)
java
public interface FileSystemElement {
void accept(Visitor visitor);
}
2. 具体元素:文件 File
java
public class File implements FileSystemElement {
private String name;
private int size;
public File(String name, int size) {
this.name = name;
this.size = size;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this); // 双分派核心
}
// getter 给访问者使用
public String getName() { return name; }
public int getSize() { return size; }
}
3. 具体元素:文件夹 Folder
java
import java.util.ArrayList;
import java.util.List;
public class Folder implements FileSystemElement {
private String name;
private List<FileSystemElement> children = new ArrayList<>();
public Folder(String name) { this.name = name; }
public void add(FileSystemElement e) { children.add(e); }
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
// 递归让子元素接受访问
for (FileSystemElement e : children) e.accept(visitor);
}
public String getName() { return name; }
public List<FileSystemElement> getChildren() { return children; }
}
4. 抽象访问者 Visitor
java
public interface Visitor {
void visit(File file);
void visit(Folder folder);
}
5. 具体访问者1:打印功能
java
public class PrintVisitor implements Visitor {
@Override
public void visit(File file) {
System.out.println("文件:" + file.getName() + " 大小:" + file.getSize());
}
@Override
public void visit(Folder folder) {
System.out.println("文件夹:" + folder.getName());
}
}
6. 具体访问者2:计算总大小
java
public class SizeVisitor implements Visitor {
private int totalSize = 0;
@Override
public void visit(File file) {
totalSize += file.getSize();
}
@Override
public void visit(Folder folder) {
// 文件夹本身不计大小,只遍历子元素
}
public int getTotalSize() { return totalSize; }
}
7. 客户端
java
public class Client {
public static void main(String[] args) {
Folder root = new Folder("root");
File f1 = new File("a.txt", 10);
File f2 = new File("b.jpg", 20);
Folder sub = new Folder("sub");
File f3 = new File("c.md", 30);
sub.add(f3);
root.add(f1);
root.add(f2);
root.add(sub);
// 打印
System.out.println("=== 打印访问者 ===");
Visitor print = new PrintVisitor();
root.accept(print);
// 统计大小
System.out.println("\n=== 大小访问者 ===");
SizeVisitor sizeVisitor = new SizeVisitor();
root.accept(sizeVisitor);
System.out.println("总大小:" + sizeVisitor.getTotalSize());
}
}
输出:
=== 打印访问者 ===
文件夹:root
文件:a.txt 大小:10
文件:b.jpg 大小:20
文件夹:sub
文件:c.md 大小:30
=== 大小访问者 ===
总大小:60
四、双分派(核心机制)
- 第一次分派:
element.accept(visitor) - 第二次分派:
visitor.visit(this)
结果:执行的是【具体元素 + 具体访问者】的组合方法
→ 这就是访问者能精准匹配类型的原因。
五、优缺点
优点
- 优秀的扩展性:加新功能只需加访问者,不改元素
- 职责单一:元素存数据,访问者做操作
- 集中式操作:可一次性对整个结构做统一处理
- 适合组合结构 + 多种算法
缺点
- 元素结构不能变:增删元素类要改所有Visitor
- 破坏封装:Visitor需要访问元素内部数据
- 复杂度高,理解成本高
六、应用场景
- 组合结构(文件树、订单树、AST)
- 需频繁新增操作,但结构稳定
- 报表、打印、审计、校验、压缩、序列化
- 编译器、IDE插件、代码检查
经典应用:
- MyBatis Mapper 解析
- Spring Event 访问式监听
- Java NIO 文件访问
- IDEA 代码检查/重构
七、访问者 VS 迭代器 VS 组合
- 组合:树形结构
- 迭代器:遍历元素
- 访问者 :对元素做不同操作,遍历+处理一体
八、总结
- 访问者模式 = 稳定结构 + 无限扩展操作
- 核心:双分派、数据与操作分离
- 最适合:组合结构 + 多算法/多报表
- 是设计模式中最难但最强大的模式之一