#访问者模式
#一个报表系统的扩展噩梦
2019年,我们的报表系统需要支持多种报表类型:
class Report {
String type; // "SALES", "INVENTORY", "FINANCIAL"
double total;
List<Row> rows;
}
void generateReport(Report report) {
if ("SALES".equals(report.type)) {
// 生成销售报表
} else if ("INVENTORY".equals(report.type)) {
// 生成库存报表
} else if ("FINANCIAL".equals(report.type)) {
// 生成财务报表
}
}每次加一个新报表类型,都要改 generateReport 方法——违反开闭原则。
访问者模式解决的就是这个问题:把对元素的操作从元素本身分离出来。
#二、访问者模式核心🔴
#2.1 基本结构
// 元素接口
interface Element {
void accept(Visitor visitor);
}
// 具体元素
class Document implements Element {
List<Element> children = new ArrayList<>();
@Override
public void accept(Visitor visitor) {
visitor.visitDocument(this);
for (Element child : children) {
child.accept(visitor);
}
}
}
class Paragraph implements Element {
String text;
@Override
public void accept(Visitor visitor) {
visitor.visitParagraph(this);
}
}
class Image implements Element {
String url;
@Override
public void accept(Visitor visitor) {
visitor.visitImage(this);
}
}
// 访问者接口
interface Visitor {
void visitDocument(Document doc);
void visitParagraph(Paragraph p);
void visitImage(Image img);
}
// 具体访问者:导出为 HTML
class HtmlExportVisitor implements Visitor {
private StringBuilder html = new StringBuilder();
@Override
public void visitDocument(Document doc) {
html.append("<html><body>");
}
@Override
public void visitParagraph(Paragraph p) {
html.append("<p>").append(p.text).append("</p>");
}
@Override
public void visitImage(Image img) {
html.append("<img src='").append(img.url).append("'/>");
}
public String getHtml() {
html.append("</body></html>");
return html.toString();
}
}
// 具体访问者:导出为 Markdown
class MarkdownExportVisitor implements Visitor {
private StringBuilder md = new StringBuilder();
@Override
public void visitDocument(Document doc) {
// 文档头
}
@Override
public void visitParagraph(Paragraph p) {
md.append(p.text).append("\n\n");
}
@Override
public void visitImage(Image img) {
md.append(".append(img.url).append(")\n");
}
public String getMarkdown() {
return md.toString();
}
}#2.2 双分派机制
// 第一次分派:调用 accept
element.accept(visitor);
// 第二次分派:accept 内部调用 visitor.visitXxx(this)
public void accept(Visitor visitor) {
visitor.visitDocument(this); // this 是 Document
}#三、编译器中的访问者🟡
#3.1 语法树遍历
// 语法树节点
interface ASTNode {
void accept(Visitor visitor);
}
class AdditionNode implements ASTNode {
ASTNode left, right;
@Override
public void accept(Visitor visitor) {
visitor.visitAddition(this);
}
}
class NumberNode implements ASTNode {
double value;
@Override
public void accept(Visitor visitor) {
visitor.visitNumber(this);
}
}
// 计算器访问者
class EvaluatorVisitor implements Visitor {
private double result;
@Override
public void visitAddition(AdditionNode node) {
node.left.accept(this);
double left = result;
node.right.accept(this);
double right = result;
result = left + right;
}
@Override
public void visitNumber(NumberNode node) {
result = node.value;
}
public double getResult() {
return result;
}
}
// 打印访问者
class PrinterVisitor implements Visitor {
private StringBuilder sb = new StringBuilder();
@Override
public void visitAddition(AdditionNode node) {
sb.append("(");
node.left.accept(this);
sb.append(" + ");
node.right.accept(this);
sb.append(")");
}
@Override
public void visitNumber(NumberNode node) {
sb.append(node.value);
}
}#3.2 文件系统统计
// 文件系统节点
interface FileSystemNode {
long getSize();
void accept(FileVisitor visitor);
}
class FileNode implements FileSystemNode {
private String name;
private long size;
@Override
public long getSize() {
return size;
}
@Override
public void accept(FileVisitor visitor) {
visitor.visitFile(this);
}
}
class DirectoryNode implements FileSystemNode {
private String name;
private List<FileSystemNode> children = new ArrayList<>();
@Override
public long getSize() {
return children.stream().mapToLong(FileSystemNode::getSize).sum();
}
@Override
public void accept(FileVisitor visitor) {
visitor.visitDirectory(this);
for (FileSystemNode child : children) {
child.accept(visitor);
}
}
}
// 文件统计访问者
class FileStatsVisitor implements FileVisitor {
private long totalSize;
private int fileCount;
private int dirCount;
@Override
public void visitFile(FileNode file) {
totalSize += file.getSize();
fileCount++;
}
@Override
public void visitDirectory(DirectoryNode dir) {
dirCount++;
}
}#四、访问者模式的优缺点🟡
#4.1 优点
| 优点 | 说明 |
|---|---|
| 开闭原则 | 新增操作只需加访问者,不改元素 |
| 单一职责 | 相关操作集中在一个访问者类 |
| 灵活性 | 可以组合多个访问者 |
#4.2 缺点
| 缺点 | 说明 |
|---|---|
| 违反开闭原则 | 新增元素要改所有访问者 |
| 复杂 | 双分派、递归调用,调试困难 |
| 耦合 | 元素必须暴露内部结构 |
【架构权衡】 访问者模式适合元素类型稳定、操作经常变化的场景(如编译器 AST)。如果元素类型也经常变化,访问者模式会导致维护困难。
#五、面试总结
| 级别 | 期望回答 |
|---|---|
| P5 | 能写出访问者模式的基本结构 |
| P6 | 能解释双分派机制 |
| P7 | 能识别访问者模式的适用场景 |