访问者模式解决什么
访问者模式(Visitor)把作用于一组对象的操作从这些对象的类里分离出来:元素类提供一个接受访问者的入口,具体操作写在访问者里,新增一种操作只需新增一个访问者,元素类不用改动。它的实现基础是双重分派——先由元素决定调用哪个接受方法,再由传入的访问者决定执行哪个处理逻辑。这个取舍有方向:操作容易增加、元素类型相对稳定时收益最大。
基本信息
| 意图 | 操作与对象结构分离,用双重分派选择处理逻辑 |
|---|---|
| 适合 | 元素类型稳定,而操作种类持续增加 |
| 代价 | 新增一种元素类型需要修改所有访问者 |
| 典型位置 | 语法树处理、报表导出、规则校验 |
典型使用场景
- 语法树处理:解析结果结构固定,而类型检查、优化、代码生成等操作不断增加
- 报表导出:节点类型稳定,导出为不同格式的需求陆续出现
- 规则校验:对同一对象结构套用多套校验规则,规则之间互不影响
- 静态分析:遍历同一棵结构,分别做统计、告警与指标提取
- 文档结构转换:同一种节点结构输出为多种目标格式
结构示意
结构示意
// 元素侧:声明接受访问者,把分派交给访问者
interface Shape {
void accept(Visitor v);
}
// 访问者侧:每种元素类型一个处理方法
interface Visitor {
void visit(Circle c);
void visit(Square s);
}
class AreaCalculator implements Visitor { /* 面积计算 */ }适用与代价
| 维度 | 有利情形 | 不利情形 |
|---|---|---|
| 元素类型 | 类型稳定,短期不新增 | 类型频繁增加,每次都要改全部访问者 |
| 操作数量 | 操作种类多且持续增加 | 只有一两种操作,直接写在元素里更简单 |
| 结构访问 | 需要遍历结构并保持封装 | 遍历逻辑本身很复杂,访问者会变得庞大 |
| 状态携带 | 访问者内可累积统计结果 | 访问者持有大量跨元素状态,难以复用 |
访问者最容易失控的地方是依赖元素内部状态:一旦访问者需要读取元素的私有细节,封装就被穿透,元素与访问者双向耦合,改动又会互相牵连。
元素类型预计会频繁增加时不要选访问者:访问者要求所有元素类型的处理分支集中定义,新增类型意味着改动全部访问者,此时用多态方法或模式匹配更合适。
落地检查清单
- 元素类型是否足够稳定,能承受访问者接口的改动成本
- 访问者只依赖元素的公开能力,不穿透内部状态
- 新增一种操作时元素类无需改动
- 访问者职责单一,不为复用而累积大量跨元素状态