设计模式(二十四)行为型:访问者模式详解
设计模式(二十四)行为型:访问者模式详解
访问者模式(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 类图:
图解说明:
Element
接口定义accept(Visitor)
。ConcreteElementX
实现accept()
,内部调用visitor.visit(this)
。Visitor
接口为每种ConcreteElement
声明一个visit
重载方法。ConcreteVisitor
实现所有visit
方法,提供具体操作。ObjectStructure
管理元素集合,并提供accept(Visitor)
遍历所有元素。
三、一个简单的Java程序实例及其UML图
以下是一个文档处理系统的示例,文档包含段落(Paragraph)、图片(Image)、表格(Table)元素,需要支持打印和统计字数操作。
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)@Overridepublic 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;}@Overridepublic 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;}@Overridepublic void accept(DocumentElementVisitor visitor) {visitor.visit(this); // this 是 Table 类型}public void validate() {System.out.println("✅ 表格数据验证: " + rows + "x" + cols + " 表格");}
}// 具体访问者:打印访问者
class PrintVisitor implements DocumentElementVisitor {@Overridepublic void visit(Paragraph paragraph) {System.out.println("🖨️ 打印段落: \"" + paragraph.getText() + "\"");}@Overridepublic void visit(Image image) {System.out.println("🖼️ 打印图片: " + image.getFilename() + " (" + image.getWidth() + "x" + image.getHeight() + ")");}@Overridepublic 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;@Overridepublic void visit(Paragraph paragraph) {String[] words = paragraph.getText().split("\\s+");int count = words.length;wordCount += count;System.out.println("📝 段落字数: \"" + paragraph.getText() + "\" -> " + count + " 词");}@Overridepublic void visit(Image image) {// 图片无文字,不计数,但可记录System.out.println("🖼️ 图片: " + image.getFilename() + " (0 词)");}@Overridepublic 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 -> visitSystem.out.println("\n--- 执行字数统计操作 ---");WordCountVisitor wordCountVisitor = new WordCountVisitor();document.accept(wordCountVisitor);System.out.println("📊 文档总字数: " + wordCountVisitor.getWordCount() + " 词");// 演示新增操作无需修改元素类System.out.println("\n--- 新增操作:元素类型检查 ---");// 只需定义新访问者class TypeCheckVisitor implements DocumentElementVisitor {@Overridepublic void visit(Paragraph paragraph) {System.out.println("✅ 元素: 段落, 内容长度: " + paragraph.getText().length());}@Overridepublic void visit(Image image) {System.out.println("✅ 元素: 图片, 文件: " + image.getFilename() + ", 尺寸: " + image.getWidth() + "x" + image.getHeight());}@Overridepublic void visit(Table table) {System.out.println("✅ 元素: 表格, 大小: " + table.getRows() + "x" + table.getCols());}}TypeCheckVisitor typeCheckVisitor = new TypeCheckVisitor();document.accept(typeCheckVisitor);}
}
实例对应的UML图(简化版)
运行说明:
DocumentElement
定义accept()
。Paragraph
,Image
,Table
实现accept()
,内部调用visitor.visit(this)
。DocumentElementVisitor
为每种元素声明visit
重载。PrintVisitor
,WordCountVisitor
实现visit
方法,提供具体操作。Document
的accept()
遍历所有元素,调用其accept()
。- 新增
TypeCheckVisitor
无需修改任何元素类,完美体现开闭原则。
四、总结
特性 | 说明 |
---|---|
核心目的 | 分离数据结构与操作,支持在不修改元素的情况下新增操作 |
实现机制 | 双重分派:元素 accept 访问者,访问者 visit 元素 |
优点 | 符合开闭原则(操作维度)、操作集中化、支持新操作、符合单一职责 |
缺点 | 违反里氏替换、元素新增困难(违反开闭原则-结构维度)、复杂性高、依赖具体类型 |
适用场景 | 稳定数据结构(如AST)、多操作需求、编译器、文档处理、报表生成 |
不适用场景 | 结构频繁变化、操作简单、避免继承/重载的语言 |
访问者模式使用建议:
- 仅在数据结构