访问者模式(Visitor)
访问者模式,说白了就是让你在不修改现有类的前提下,给这些类增加新的操作。我刚开始接触这个模式时,觉得它有点绕——为什么要绕这么大一圈?后来在项目中遇到一个实际问题,才真正体会到它的价值。
模式定义
访问者模式表示一个作用于某对象结构中的各元素的操作。它允许你在不改变各元素类的前提下,定义作用于这些元素的新操作。
核心思想其实很简单:把操作从对象结构中分离出来。你想想看,如果有一组稳定的对象结构,但你需要不断给它们添加新功能,直接修改每个类显然不是好办法。访问者模式就是来解决这个问题的。
模式结构
访问者模式涉及以下几个角色:
- Visitor(访问者):声明一组访问方法,每个方法对应一个具体的元素类型
- ConcreteVisitor(具体访问者):实现具体的访问逻辑
- Element(元素):声明一个accept方法,接收访问者作为参数
- ConcreteElement(具体元素):实现accept方法
- ObjectStructure(对象结构):管理元素集合,提供遍历方式
关键点:访问者模式使用了"双重分派"机制。第一次分派是调用元素的accept方法,第二次分派是调用访问者的visit方法。这样就把操作和数据结构解耦了。
SVG:访问者模式核心结构图
实际应用场景
我在项目中遇到过这样一个场景:一个文档系统包含多种节点类型——文本节点、图片节点、表格节点。需要对这些节点做各种操作:导出HTML、导出PDF、统计字数、检查链接有效性等等。
如果不用访问者模式,每增加一个操作就要修改所有节点类。用了访问者模式后,每个操作就是一个新的访问者,节点类完全不用动。
适用场景:
- 对象结构稳定,但需要频繁添加新操作
- 需要对一个对象结构中的不同元素执行不同操作
- 操作涉及多个不相关的类,不想把这些操作分散到各个类中
代码示例
来看一个文件系统的例子。我们有文件和文件夹两种元素,需要做两种操作:计算大小和打印结构。
// 访问者接口
interface Visitor {
void visitFile(File file);
void visitFolder(Folder folder);
}
// 元素接口
interface Element {
void accept(Visitor visitor);
}
// 文件元素
class File implements Element {
private String name;
private int size;
public File(String name, int size) {
this.name = name;
this.size = size;
}
public String getName() { return name; }
public int getSize() { return size; }
@Override
public void accept(Visitor visitor) {
visitor.visitFile(this);
}
}
// 文件夹元素
class Folder implements Element {
private String name;
private List<Element> children = new ArrayList<>();
public Folder(String name) {
this.name = name;
}
public void add(Element element) {
children.add(element);
}
public String getName() { return name; }
public List<Element> getChildren() { return children; }
@Override
public void accept(Visitor visitor) {
visitor.visitFolder(this);
}
}
// 具体访问者:计算大小
class SizeCalculator implements Visitor {
private int totalSize = 0;
@Override
public void visitFile(File file) {
totalSize += file.getSize();
}
@Override
public void visitFolder(Folder folder) {
for (Element child : folder.getChildren()) {
child.accept(this);
}
}
public int getTotalSize() { return totalSize; }
}
// 具体访问者:打印结构
class StructurePrinter implements Visitor {
private int indent = 0;
@Override
public void visitFile(File file) {
printIndent();
System.out.println(file.getName() + " (" + file.getSize() + " bytes)");
}
@Override
public void visitFolder(Folder folder) {
printIndent();
System.out.println("[文件夹] " + folder.getName());
indent++;
for (Element child : folder.getChildren()) {
child.accept(this);
}
indent--;
}
private void printIndent() {
for (int i = 0; i < indent; i++) {
System.out.print(" ");
}
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
Folder root = new Folder("root");
root.add(new File("readme.txt", 100));
root.add(new File("logo.png", 500));
Folder src = new Folder("src");
src.add(new File("main.java", 200));
src.add(new File("utils.java", 150));
root.add(src);
// 计算大小
SizeCalculator sizeCalc = new SizeCalculator();
root.accept(sizeCalc);
System.out.println("总大小: " + sizeCalc.getTotalSize() + " bytes");
// 打印结构
StructurePrinter printer = new StructurePrinter();
root.accept(printer);
}
}
优缺点分析
| 优点 | 缺点 |
|---|---|
| 符合开闭原则:新增操作无需修改现有元素类 | 增加新元素类困难:需要修改所有访问者接口 |
| 将相关操作集中到一个访问者中 | 访问者需要知道元素内部细节,破坏了封装 |
| 便于添加新操作 | 元素数量较多时,访问者接口会变得臃肿 |
避坑指南:我曾经在一个项目中滥用访问者模式,把本来可以用简单循环解决的问题硬套上了访问者模式。结果代码变得又长又绕。记住:访问者模式适用于对象结构稳定但操作频繁变化的场景。如果元素类型经常变化,就别用这个模式。
与策略模式的对比
很多人会把访问者模式和策略模式搞混。我简单说说区别:
- 策略模式:针对同一类型对象,封装不同的算法。比如对订单使用不同的折扣策略。
- 访问者模式:针对不同类型对象,封装不同的操作。比如对文件、文件夹、链接分别做导出操作。
说白了,策略模式是"一个对象,多种算法";访问者模式是"多种对象,多种操作"。
实际项目中的思考
我记得有一次做代码生成器,需要遍历抽象语法树(AST)。AST的节点类型很多——变量声明、函数定义、条件语句等等。每个节点都需要做类型检查、代码生成、优化等操作。
如果用传统的if-else或者switch,代码会变得极其混乱。访问者模式完美解决了这个问题:每个操作就是一个访问者,AST节点完全不用修改。后来要增加一个新的"代码混淆"功能,只需要写一个新的访问者就行,AST的代码一行都没动。
嗯,这里要注意一点:访问者模式虽然强大,但不要为了用而用。如果你的对象结构经常变化,或者操作数量很少,用简单的多态或者策略模式可能更合适。
公众号:蓝海资料掘金营,微信deep3321