设计模式(二十四)行为型:访问者模式详解
访问者模式(Visitor Pattern)是 GoF 23 种设计模式中最具争议性但也最强大的行为型模式之一,其核心价值在于将作用于某种数据结构中的各元素的操作分离出来,封装到一个独立的访问者对象中,使得在不改变元素类的前提下可以定义新的操作 。它通过"双重分派"(Double Dispatch)机制,解决了在静态类型语言中对异构对象集合 进行多态操作扩展的难题。访问者模式是构建编译器(语法树遍历)、文档处理系统、复杂报表生成、UI 渲染引擎、静态代码分析工具等系统的理想选择,是实现"开闭原则"在操作维度上的终极体现。
一、详细介绍
访问者模式解决的是"一个数据结构(如对象树或列表)包含多种类型的元素,且需要对这些元素执行多种不同的、与元素本身无关的操作,且这些操作可能频繁新增"的问题。在传统设计中,通常将操作直接定义在元素类中。这导致:
- 违反单一职责原则:元素类承担了数据和多种操作的职责。
- 难以扩展操作:新增操作需要修改所有元素类,违反开闭原则。
- 代码分散:同一操作的逻辑分散在多个元素类中。
访问者模式的核心思想是:将"数据结构"与"作用于数据的操作"解耦。数据结构中的元素接受一个访问者对象作为参数,回调访问者对象中对应其类型的方法。新的操作只需添加新的访问者类,无需修改任何元素类。
该模式包含以下核心角色:
- Visitor(访问者接口) :声明一组
visit()
方法,每个方法对应一种具体的元素类型(如visit(ElementA)
,visit(ElementB)
)。它定义了所有可执行操作的抽象接口。 - ConcreteVisitor(具体访问者) :实现
Visitor
接口,为每种元素类型提供具体的操作实现。每个具体访问者代表一种独立的操作(如打印、计算、导出)。 - Element(元素接口) :声明一个
accept(Visitor)
方法,允许访问者"访问"自身。 - ConcreteElementA, ConcreteElementB, ...(具体元素) :实现
Element
接口,实现accept()
方法。在accept()
中,调用访问者的visit(this)
方法,将自身作为参数传入,触发正确的visit
方法(关键:this
的静态类型是当前类,实现双重分派)。 - ObjectStructure(对象结构) :可选角色,表示包含元素的集合或复合结构(如树、列表)。它提供一个接口,允许访问者遍历其所有元素,并调用每个元素的
accept()
方法。
访问者模式的关键优势:
- 符合开闭原则(操作维度) :新增操作只需添加新的
ConcreteVisitor
,无需修改Element
或ConcreteElement
。 - 符合单一职责原则 :元素类只负责数据和
accept
,操作逻辑集中在访问者中。 - 操作集中化:同一操作的逻辑集中在单个访问者类中,易于理解、维护和复用。
- 支持新操作:可以轻松添加如打印、统计、转换、验证等新操作。
访问者模式的关键挑战(双重分派):
- 第一重分派 :在
ObjectStructure
中,调用element.accept(visitor)
。由于element
是多态的,accept()
的调用会根据element
的实际类型分派到ConcreteElementA.accept()
或ConcreteElementB.accept()
。 - 第二重分派 :在
ConcreteElementX.accept()
中,调用visitor.visit(this)
。this
的静态类型 是ConcreteElementX
,因此编译器会选择visitor
上参数类型为ConcreteElementX
的visit
方法。即使visitor
是多态的,visit
方法的重载选择在编译时基于this
的静态类型确定。
缺点与限制:
- 违反里氏替换原则 :
accept()
方法暴露了具体类型。 - 元素类难以修改 :新增元素类型需要修改所有
Visitor
接口及其所有实现类,违反开闭原则(结构维度)。 - 复杂性高:理解双重分派和模式结构需要较高心智负担。
- 过度设计:对于简单操作或稳定结构,可能不必要。
访问者模式适用于:
- 数据结构稳定,但操作频繁变化(如编译器 AST)。
- 需要对复杂对象结构执行多种不同的操作。
- 操作需要访问元素的私有成员(访问者可通过友元或公共方法访问)。
- 需要避免在元素类中堆积大量无关操作。
二、访问者模式的UML表示
以下是访问者模式的标准 UML 类图:
implements implements implements implements implements calls visit() calls visit() calls visit() contains calls accept() <<interface>> Visitor +visit(elementA: ConcreteElementA) +visit(elementB: ConcreteElementB) +visit(elementC: ConcreteElementC) ConcreteVisitor1 +visit(elementA: ConcreteElementA) +visit(elementB: ConcreteElementB) +visit(elementC: ConcreteElementC) ConcreteVisitor2 +visit(elementA: ConcreteElementA) +visit(elementB: ConcreteElementB) +visit(elementC: ConcreteElementC) <<interface>> Element +accept(visitor: Visitor) ConcreteElementA +accept(visitor: Visitor) +operationA() ConcreteElementB +accept(visitor: Visitor) +operationB() ConcreteElementC +accept(visitor: Visitor) +operationC() ObjectStructure -elements: List<Element> +attach(element: Element) +detach(element: Element) +accept(visitor: Visitor)
图解说明:
Element
接口定义accept(Visitor)
。ConcreteElementX
实现accept()
,内部调用visitor.visit(this)
。Visitor
接口为每种ConcreteElement
声明一个visit
重载方法。ConcreteVisitor
实现所有visit
方法,提供具体操作。ObjectStructure
管理元素集合,并提供accept(Visitor)
遍历所有元素。
三、一个简单的Java程序实例及其UML图
以下是一个文档处理系统的示例,文档包含段落(Paragraph)、图片(Image)、表格(Table)元素,需要支持打印和统计字数操作。
Java 程序实例
java
// 访问者接口
interface DocumentElementVisitor {
void visit(Paragraph paragraph);
void visit(Image image);
void visit(Table table);
}
// 元素接口
interface DocumentElement {
void accept(DocumentElementVisitor visitor);
}
// 具体元素:段落
class Paragraph implements DocumentElement {
private String text;
public Paragraph(String text) {
this.text = text;
}
public String getText() {
return text;
}
// accept 实现:回调访问者,传入自身(this)
@Override
public void accept(DocumentElementVisitor visitor) {
visitor.visit(this); // 双重分派的关键:this 是 Paragraph 类型
}
public void spellCheck() {
System.out.println("🔍 段落拼写检查: " + text);
}
}
// 具体元素:图片
class Image implements DocumentElement {
private String filename;
private int width;
private int height;
public Image(String filename, int width, int height) {
this.filename = filename;
this.width = width;
this.height = height;
}
public String getFilename() {
return filename;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
@Override
public void accept(DocumentElementVisitor visitor) {
visitor.visit(this); // this 是 Image 类型
}
public void compress() {
System.out.println("🗜️ 压缩图片: " + filename);
}
}
// 具体元素:表格
class Table implements DocumentElement {
private String[][] data;
private int rows;
private int cols;
public Table(String[][] data) {
this.data = data;
this.rows = data.length;
this.cols = data.length > 0 ? data[0].length : 0;
}
public String[][] getData() {
return data;
}
public int getRows() {
return rows;
}
public int getCols() {
return cols;
}
@Override
public void accept(DocumentElementVisitor visitor) {
visitor.visit(this); // this 是 Table 类型
}
public void validate() {
System.out.println("✅ 表格数据验证: " + rows + "x" + cols + " 表格");
}
}
// 具体访问者:打印访问者
class PrintVisitor implements DocumentElementVisitor {
@Override
public void visit(Paragraph paragraph) {
System.out.println("🖨️ 打印段落: \"" + paragraph.getText() + "\"");
}
@Override
public void visit(Image image) {
System.out.println("🖼️ 打印图片: " + image.getFilename() +
" (" + image.getWidth() + "x" + image.getHeight() + ")");
}
@Override
public void visit(Table table) {
System.out.println("📊 打印表格: " + table.getRows() + " 行, " + table.getCols() + " 列");
for (int i = 0; i < table.getRows(); i++) {
for (int j = 0; j < table.getCols(); j++) {
System.out.print("[" + table.getData()[i][j] + "] ");
}
System.out.println();
}
}
}
// 具体访问者:字数统计访问者
class WordCountVisitor implements DocumentElementVisitor {
private int wordCount = 0;
@Override
public void visit(Paragraph paragraph) {
String[] words = paragraph.getText().split("\\s+");
int count = words.length;
wordCount += count;
System.out.println("📝 段落字数: \"" + paragraph.getText() + "\" -> " + count + " 词");
}
@Override
public void visit(Image image) {
// 图片无文字,不计数,但可记录
System.out.println("🖼️ 图片: " + image.getFilename() + " (0 词)");
}
@Override
public void visit(Table table) {
int count = 0;
for (int i = 0; i < table.getRows(); i++) {
for (int j = 0; j < table.getCols(); j++) {
if (table.getData()[i][j] != null && !table.getData()[i][j].trim().isEmpty()) {
count += table.getData()[i][j].split("\\s+").length;
}
}
}
wordCount += count;
System.out.println("📊 表格字数: " + count + " 词");
}
// 获取统计结果
public int getWordCount() {
return wordCount;
}
}
// 对象结构:文档
class Document {
private java.util.List<DocumentElement> elements = new java.util.ArrayList<>();
public void addElement(DocumentElement element) {
elements.add(element);
}
public void removeElement(DocumentElement element) {
elements.remove(element);
}
// 接受访问者,遍历所有元素
public void accept(DocumentElementVisitor visitor) {
for (DocumentElement element : elements) {
element.accept(visitor);
}
}
}
// 客户端使用示例
public class VisitorPatternDemo {
public static void main(String[] args) {
System.out.println("📄 文档处理系统 - 访问者模式示例\n");
// 创建文档和元素
Document document = new Document();
document.addElement(new Paragraph("这是一个关于设计模式的文档。"));
document.addElement(new Image("diagram.png", 800, 600));
document.addElement(new Paragraph("访问者模式非常强大。"));
document.addElement(new Table(new String[][]{
{"模式", "类型", "用途"},
{"访问者", "行为型", "分离操作"},
{"策略", "行为型", "替换算法"}
}));
document.addElement(new Paragraph("总结:访问者模式适用于稳定结构。"));
// 使用打印访问者
System.out.println("--- 执行打印操作 ---");
PrintVisitor printVisitor = new PrintVisitor();
document.accept(printVisitor); // 遍历元素,触发 accept -> visit
System.out.println("\n--- 执行字数统计操作 ---");
WordCountVisitor wordCountVisitor = new WordCountVisitor();
document.accept(wordCountVisitor);
System.out.println("📊 文档总字数: " + wordCountVisitor.getWordCount() + " 词");
// 演示新增操作无需修改元素类
System.out.println("\n--- 新增操作:元素类型检查 ---");
// 只需定义新访问者
class TypeCheckVisitor implements DocumentElementVisitor {
@Override
public void visit(Paragraph paragraph) {
System.out.println("✅ 元素: 段落, 内容长度: " + paragraph.getText().length());
}
@Override
public void visit(Image image) {
System.out.println("✅ 元素: 图片, 文件: " + image.getFilename() +
", 尺寸: " + image.getWidth() + "x" + image.getHeight());
}
@Override
public void visit(Table table) {
System.out.println("✅ 元素: 表格, 大小: " + table.getRows() + "x" + table.getCols());
}
}
TypeCheckVisitor typeCheckVisitor = new TypeCheckVisitor();
document.accept(typeCheckVisitor);
}
}
实例对应的UML图(简化版)
implements implements implements implements implements implements calls visit() calls visit() calls visit() contains calls accept() <<interface>> DocumentElementVisitor +visit(paragraph: Paragraph) +visit(image: Image) +visit(table: Table) PrintVisitor +visit(paragraph: Paragraph) +visit(image: Image) +visit(table: Table) WordCountVisitor -wordCount: int +visit(paragraph: Paragraph) +visit(image: Image) +visit(table: Table) +getWordCount() TypeCheckVisitor +visit(paragraph: Paragraph) +visit(image: Image) +visit(table: Table) <<interface>> DocumentElement +accept(visitor: DocumentElementVisitor) Paragraph -text: String +accept(visitor: DocumentElementVisitor) +getText() Image -filename: String -width: int -height: int +accept(visitor: DocumentElementVisitor) +getFilename() Table -data: String[][] +accept(visitor: DocumentElementVisitor) +getData() Document -elements: List<DocumentElement> +addElement(element: DocumentElement) +accept(visitor: DocumentElementVisitor)
运行说明:
DocumentElement
定义accept()
。Paragraph
,Image
,Table
实现accept()
,内部调用visitor.visit(this)
。DocumentElementVisitor
为每种元素声明visit
重载。PrintVisitor
,WordCountVisitor
实现visit
方法,提供具体操作。Document
的accept()
遍历所有元素,调用其accept()
。- 新增
TypeCheckVisitor
无需修改任何元素类,完美体现开闭原则。
四、总结
特性 | 说明 |
---|---|
核心目的 | 分离数据结构与操作,支持在不修改元素的情况下新增操作 |
实现机制 | 双重分派:元素 accept 访问者,访问者 visit 元素 |
优点 | 符合开闭原则(操作维度)、操作集中化、支持新操作、符合单一职责 |
缺点 | 违反里氏替换、元素新增困难(违反开闭原则-结构维度)、复杂性高、依赖具体类型 |
适用场景 | 稳定数据结构(如AST)、多操作需求、编译器、文档处理、报表生成 |
不适用场景 | 结构频繁变化、操作简单、避免继承/重载的语言 |
访问者模式使用建议:
- 仅在数据结构