访问者模式-Visitor Pattern
序言
一个文件系统里有文件和目录:文件有大小,目录可以包含文件和子目录。现在有两个需求——统计所有文件的总大小、把整个目录树导出成树状文本。这两个操作遍历的都是相同的元素结构(File / Directory),只是对每个元素执行的操作不同。
如果直接在每个元素类里实现"计算大小""导出文本",元素类会被各种不相关的操作塞满,而且每新增一个操作(比如"统计文件数量")都要修改所有元素类——违反开闭原则。
访问者模式(Visitor Pattern)给出了优雅的解法:把"操作"从元素类中分离出来,封装成独立的访问者对象;元素类只提供一个 accept(visitor) 方法,让访问者"进入"元素执行操作。这样,新增操作只需要新增一个访问者类,元素类一个字节都不用改。
定义
Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
两个关键点:
- 操作与元素分离:操作封装在访问者中,元素只负责"接待"访问者;
- 不改元素类:新增操作不需要修改任何元素类,只需要新增访问者。
结构
让我们从一个问题开始
假设文件系统中有 File(文件)和 Directory(目录)两类元素。要计算总大小,最直接的方式是在元素类中实现:
public class File {
private final long size;
public long calculateSize() {
return size;
}
}
public class Directory {
private final List<Element> children = new ArrayList<>();
public long calculateSize() {
long total = 0;
for (Element child : children) {
total += child.calculateSize();
}
return total;
}
}
然后需求又来了:要导出树状文本、要统计文件个数、要找出最大的文件……每个需求都要往元素类里加方法。元素类越来越多与"文件系统"无关的操作,而且每加一个操作都要动所有元素类。
访问者模式的结构
把操作放进访问者,元素只提供 accept 方法:
各个角色的职责如下:
- Visitor(抽象访问者):为每种元素声明一个访问方法(
visitFile、visitDirectory),对应图中的Visitor。 - ConcreteVisitor(具体访问者):实现每种元素上的具体操作,对应图中的
SizeCalculatorVisitor、TreeExportVisitor。 - Element(元素):声明
accept(visitor)方法,接受访问者访问,对应图中的Element。 - ConcreteElement(具体元素):实现
accept,在方法内部调用visitor.visitXxx(this),对应图中的File、Directory。 - ObjectStructure(对象结构):持有元素集合,供访问者遍历,对应图中的
Directory(树结构本身)。
双分派(Double Dispatch)
访问者模式的关键机制是双分派:
- 第一次分派:客户端调用
element.accept(visitor),由元素的动态类型决定进入哪个元素的accept(File还是Directory); - 第二次分派:
accept内部调用visitor.visitXxx(this),由访问者的动态类型决定执行哪个操作(SizeCalculatorVisitor还是TreeExportVisitor)。
正是双分派让"在不知道具体元素类型的情况下,也能对每种元素执行正确操作"成为可能。
代码实现
我们来实现文件系统的"计算大小"和"导出树"两个访问者。
定义元素接口和具体元素
public interface Element {
void accept(Visitor visitor);
}
import java.util.ArrayList;
import java.util.List;
public class File implements Element {
private final String name;
private final long size;
public File(String name, long size) {
this.name = name;
this.size = size;
}
@Override
public void accept(Visitor visitor) {
// 第一次分派在这里完成:this 是 File
visitor.visitFile(this);
}
public String getName() {
return name;
}
public long getSize() {
return size;
}
}
public class Directory implements Element {
private final String name;
private final List<Element> children = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
public void add(Element element) {
children.add(element);
}
@Override
public void accept(Visitor visitor) {
// 第一次分派:this 是 Directory
visitor.visitDirectory(this);
}
public String getName() {
return name;
}
public List<Element> getChildren() {
return children;
}
}
定义访问者接口和具体访问者
public interface Visitor {
void visitFile(File file);
void visitDirectory(Directory directory);
}
public class SizeCalculatorVisitor implements Visitor {
private long totalSize = 0;
@Override
public void visitFile(File file) {
// 文件:累加大小
totalSize += file.getSize();
}
@Override
public void visitDirectory(Directory directory) {
// 目录:遍历子元素(子元素会再次触发 accept 分派)
for (Element child : directory.getChildren()) {
child.accept(this);
}
}
public long getTotalSize() {
return totalSize;
}
}
public class TreeExportVisitor implements Visitor {
private final StringBuilder builder = new StringBuilder();
private int depth = 0;
@Override
public void visitFile(File file) {
builder.append(" ".repeat(depth))
.append("📄 ").append(file.getName())
.append(" (").append(file.getSize()).append(" bytes)\n");
}
@Override
public void visitDirectory(Directory directory) {
builder.append(" ".repeat(depth))
.append("📁 ").append(directory.getName()).append("/\n");
depth++;
for (Element child : directory.getChildren()) {
child.accept(this);
}
depth--;
}
public String getTree() {
return builder.toString();
}
}