深入浅出设计模式【二十三、访问者模式】
访问者模式详解
一、访问者模式介绍
访问者模式是一种行为型设计模式,它允许你将算法与对象结构分离。该模式的核心思想是:定义不改变对象结构的情况下操作结构中元素的新操作。
访问者模式解决了以下关键问题:
- 当需要在不修改现有类的前提下向类层次结构添加新功能
- 当对象结构包含许多不同类型的对象,需要对这些对象执行依赖于其具体类型的操作
- 需要在多个不同类上执行相同操作,但每个类的操作实现不同
在软件设计中,访问者模式是典型的**“双重分派”**模式,它提供了一种将算法与对象结构分离的方法,符合开闭原则(对扩展开放,对修改关闭)。
二、核心概念与意图
核心概念
-
访问者(Visitor)
- 声明对对象结构中元素操作的访问方法
- 为每种具体元素类定义
visit方法 - 可以累积状态(实现状态访问者)
-
具体访问者(ConcreteVisitor)
- 实现访问者声明的每个操作
- 每个操作实现对象结构中一个类对应算法的片段
-
元素(Element)
- 声明
accept方法,接收一个访问者参数 - 方法签名:
accept(Visitor v)
- 声明
-
具体元素(ConcreteElement)
- 实现
accept方法,典型实现:v.visit(this) - 通过将自身作为参数传递给访问者,实现双重分派
- 实现
-
对象结构(Object Structure)
- 能枚举其元素
- 可以是一个集合、复合结构或列表
- 提供高层接口允许访问者访问其元素
意图
- 定义作用于对象结构中各元素的操作,但不改变元素类
- 封装分布在多个类中的相关操作
- 在对象结构稳定但操作可能频繁变化时保持灵活性
三、适用场景剖析
访问者模式适用于:
-
处理复杂对象结构
- 当对象结构包含许多具有不同接口的类,需要对所有对象执行操作
- 例如:编译器中的抽象语法树(AST)处理
-
跨类结构的横切关注点
- 需要在不修改类的前提下添加相关操作
- 例如:统计分析、日志记录、持久化操作
-
行为分离
- 当对象结构相对稳定但操作频繁变化
- 例如:文档导出功能(PDF、HTML、纯文本等格式)
-
累积状态操作
- 当算法需要跨多个对象累积状态时
- 例如:计算对象集合的总价格或总数
不适用场景:
- 对象结构频繁变化(添加新元素会破坏所有访问者)
- 元素接口不稳定(改变元素接口会破坏所有访问者)
- 简单对象结构(使用访问者模式会过度复杂化)
四、UML类图解析(Mermaid)
流程说明:
- 客户端创建具体访问者对象
- 客户端创建对象结构并填充具体元素
- 客户端调用对象结构的
accept方法,传入访问者 - 对象结构遍历其元素,对每个元素调用
accept(visitor) - 元素调用访问者的
visit(this)方法(双重分派) - 访问者对元素执行具体操作
五、各种实现方式及其优缺点
1. 标准实现(接口+类层次)
最常见实现方式,符合访问者模式的原始定义:
// 元素接口
public interface Element {
void accept(Visitor visitor);
}
// 具体元素A
public class ConcreteElementA implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String operationA() {
return "Element A operation";
}
}
// 具体元素B
public class ConcreteElementB implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String operationB() {
return "Element B operation";
}
}
// 访问者接口
public interface Visitor {
void visit(ConcreteElementA element);
void visit(ConcreteElementB element);
}
// 具体访问者1
public class ConcreteVisitor1 implements Visitor {
@Override
public void visit(ConcreteElementA element) {
System.out.println("Visitor1: " + element.operationA());
}
@Override
public void visit(ConcreteElementB element) {
System.out.println("Visitor1: " + element.operationB());
}
}
// 具体访问者2
public class ConcreteVisitor2 implements Visitor {
@Override
public void visit(ConcreteElementA element) {
System.out.println("Visitor2 processing: " + element.operationA());
}
@Override
public void visit(ConcreteElementB element) {
System.out.println("Visitor2 processing: " + element.operationB());
}
}
// 对象结构
public class ObjectStructure {
private List<Element> elements = new ArrayList<>();
public void addElement(Element element) {
elements.add(element);
}
public void accept(Visitor visitor) {
elements.forEach(e -> e.accept(visitor));
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
ObjectStructure structure = new ObjectStructure();
structure.addElement(new ConcreteElementA());
structure.addElement(new ConcreteElementB());
Visitor visitor1 = new ConcreteVisitor1();
structure.accept(visitor1);
Visitor visitor2 = new ConcreteVisitor2();
structure.accept(visitor2);
}
}
优点:
- 清晰分离数据结构与操作
- 添加新操作只需创建新访问者类
- 操作相关代码集中在一处
- 符合开闭原则(操作扩展不影响元素类)
缺点:
- 添加新元素需要修改所有访问者接口
- 违反依赖倒置原则(访问者依赖具体元素类)
- 元素类需暴露足够信息供访问者操作
2. 反射访问者实现(Java反射)
利用反射机制绕过接口方法声明限制:
public class ReflectiveVisitor implements Visitor {
public void visit(Element element) {
try {
Method visitMethod = getClass().getMethod("visit", element.getClass());
visitMethod.invoke(this, element);
} catch (Exception e) {
// 处理未实现的情况
}
}
public void visit(ConcreteElementA element) {
System.out.println("Visiting ElementA: " + element.operationA());
}
public void visit(ConcreteElementB element) {
System.out.println("Visiting ElementB: " + element.operationB());
}
}
优点:
- 添加新元素只需实现新方法,不修改接口
- 避免元素接口膨胀
缺点:
- 类型检查在运行时而非编译时
- 反射性能开销
- 方法签名不明确降低代码可读性
- 错误处理复杂
3. Lambda实现(Java 8+)
对于简单操作,可使用函数式接口简化:
public class LambdaVisitor implements ElementVisitor {
private Map<Class<? extends Element>, Consumer<? extends Element>> handlers = new HashMap<>();
public <T extends Element> void registerHandler(Class<T> type, Consumer<T> handler) {
handlers.put(type, handler);
}
@Override
public void visit(Element element) {
Consumer<Element> handler = (Consumer<Element>) handlers.get(element.getClass());
if (handler != null) {
handler.accept(element);
}
}
}
// 使用
LambdaVisitor visitor = new LambdaVisitor();
visitor.registerHandler(ConcreteElementA.class,
element -> System.out.println("Lambda visiting A: " + element.operationA()));
优点:
- 简洁灵活
- 避免创建大量小类
- 适合一次性操作
缺点:
- 不适用于复杂状态访问者
- 类型转换不安全
- 操作逻辑分散
六、最佳实践
-
识别稳定部分
- 访问者模式最适合对象结构稳定的场景
- 元素类变化频率应远低于访问者
-
避免访问者状态污染
- 访问者应为无状态或方法局部状态
- 全局状态使用独立数据结构管理
-
访问者接口版本化
- 使用版本命名:
VisitorV1,VisitorV2 - 旧版访问者处理遗留元素(Deprecated元素)
- 使用版本命名:
-
组合访问者
- 创建组合访问者统一处理多种操作
- 减少对对象结构的遍历次数
-
空访问者实现
- 提供默认访问者避免实现所有方法
public abstract class DefaultVisitor implements Visitor { @Override public void visit(ConcreteElementA element) {} @Override public void visit(ConcreteElementB element) {} } -
文档化访问者契约
- 清晰说明访问操作的前提和后置条件
- 记录操作之间的依赖关系
七、在开发中的演变和应用
1. 语言扩展应用
- 编译器设计:抽象语法树(AST)处理
- 注解处理器:Java注解处理API(APT)
- 语法分析器:ANTLR等解析器生成器
2. 数据转换框架
- 对象序列化:Jackson/Gson等JSON库中的序列化器
- 协议转换:不同API/协议间转换(REST↔gRPC)
- ETL工具:数据提取转换过程
3. 基础设施自动化
- 基础设施即代码:Terraform资源访问
- 配置管理:统一配置校验、部署处理
- 安全扫描:对系统各组件执行安全检查
4. UI渲染框架
- DOM访问器:浏览器DOM处理优化
- UI组件树渲染:React/Vue等虚拟DOM比较
八、真实开发案例
1. Java语言内部
javax.lang.model.element.ElementVisitor
Java标准库中用于注解处理的访问者API:
public interface ElementVisitor<R, P> {
R visit(Element e, P p);
R visitPackage(PackageElement e, P p);
R visitType(TypeElement e, P p);
R visitVariable(VariableElement e, P p);
R visitExecutable(ExecutableElement e, P p);
// 其他元素类型
}
编译器使用此接口实现编译时注解处理、代码生成等高级特性。
2. Java字节码工程库
ASM框架:使用访问者模式处理字节码
public class ClassPrinter extends ClassVisitor {
public ClassPrinter() {
super(Opcodes.ASM9);
}
@Override
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) {
System.out.println(name + " extends " + superName + " {");
}
@Override
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
System.out.println(" " + name + desc);
return null;
}
@Override
public void visitEnd() {
System.out.println("}");
}
}
// 使用
ClassReader reader = new ClassReader("java.lang.String");
ClassPrinter printer = new ClassPrinter();
reader.accept(printer, 0);
3. Spring框架
Bean定义访问器BeanDefinitionVisitor
在Spring IOC容器内部实现中:
public class CustomBeanDefinitionVisitor extends BeanDefinitionVisitor {
@Override
public void visitBeanDefinition(BeanDefinition beanDefinition) {
// 在Bean定义加载时处理定制逻辑
}
@Override
public void visitPropertyValues(PropertyValues pvs) {
// 处理属性值
}
}
用于在Bean加载过程中实现定制处理,如配置加密解密。
4. Eclipse建模框架(EMF)
EMF.EList的访问器
在模型驱动开发中:
public class ModelVisitor {
void visit(EObject obj) {
for (EStructuralFeature feature : obj.eClass().getEAllStructuralFeatures()) {
Object value = obj.eGet(feature);
if (value instanceof EObject) {
visit((EObject)value);
} else if (value instanceof EList) {
((EList<?>)value).forEach(this::processElement);
}
}
}
}
5. 数据库访问层
SQL AST访问器
在复杂SQL构建器中:
public class SQLExpressionVisitor {
void visit(SelectStatement stmt) { ... }
void visit(FromClause from) { ... }
void visit(WhereClause where) { ... }
void visit(JoinExpression join) { ... }
}
MyBatis等框架使用此模式解析和优化SQL语句。
九、总结
| 方面 | 总结 |
|---|---|
| 模式类型 | 行为型设计模式 |
| 核心意图 | 分离数据结构与操作,允许添加新操作而不改变数据结构 |
| 关键角色 | 访问者、具体访问者、元素、具体元素、对象结构 |
| 核心机制 | 双重分派(元素accept方法调用访问者visit方法) |
| 主要优点 | 1. 开闭原则(添加新操作容易) 2. 操作相关代码集中 3. 跨元素类累积状态简单 4. 分离无关行为 |
| 主要缺点 | 1. 违反封装性 2. 添加新元素困难 3. 可能破坏对象完整性 4. 学习曲线陡峭 |
| 适用场景 | 对象结构稳定但操作频繁变化;跨类型元素操作;编译器/AST处理 |
| 最佳实践 | 1. 保持对象结构稳定 2. 避免访问者状态污染 3. 使用组合访问者减少遍历 4. 文档化访问者契约 |
| 演变应用 | 1. 编译器/解释器设计 2. 数据序列化框架 3. 基础设施自动化 4. UI渲染优化 |
| 工业案例 | 1. Java注解处理器 2. ASM字节码操作 3. Spring Bean定义处理 4. SQL AST处理 |
访问者模式是处理稳定对象结构和变化操作的终极解决方案,尤其在以下场景表现突出:
- 需要向现有类层次添加新操作
- 操作需要跨越不同类实现
- 对象结构复杂但相对稳定
尽管有其局限性(特别是对对象结构变化的敏感性),访问者模式在编译器设计、序列化框架和复杂数据转换等场景中仍是无可替代的强大工具。正确使用访问者模式可以大幅提高系统的可扩展性和可维护性,但需要谨慎权衡其对对象封装性的影响。
在Java生态系统中,访问者模式广泛用于编译器(javac的注解处理)、字节码工程(ASM)和Spring框架的核心基础设施中。理解访问者模式的精髓,将帮助你在面临适当场景时做出更优雅的架构决策。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120411

浙公网安备 33010602011771号