如何用 Java 实现访问者模式?
简化版
Java 实现访问者模式通常先定义元素接口 accept(Visitor),再定义访问者接口 visitXxx(ConcreteElement),具体元素在 accept 中调用访问者方法,具体访问者实现不同操作。
详细版
实现步骤:
- 定义 Visitor 接口,为每类元素声明 visit 方法。
- 定义 Element 接口,声明 accept 方法。
- 每个 ConcreteElement 实现 accept,并调用对应 visit 方法。
- 定义 ConcreteVisitor,实现某种具体操作。
- 对象结构遍历元素并调用 accept。
示例场景:文件资源处理。
interface ResourceFile {
void accept(Visitor visitor);
}
interface Visitor {
void visitPdf(PdfFile file);
void visitWord(WordFile file);
}
核心代码在元素:
class PdfFile implements ResourceFile {
public void accept(Visitor visitor) {
visitor.visitPdf(this);
}
}
完整版教学
一、先定义访问者接口
访问者接口要体现“能访问哪些元素”:
interface Visitor {
void visitPdf(PdfFile file);
void visitWord(WordFile file);
}
这里每一个 visit 方法都对应一种具体元素类型。如果元素类型稳定,这个接口也会相对稳定。
二、定义元素接口和具体元素
interface ResourceFile {
void accept(Visitor visitor);
}
class PdfFile implements ResourceFile {
private String path;
public PdfFile(String path) {
this.path = path;
}
public String getPath() {
return path;
}
public void accept(Visitor visitor) {
visitor.visitPdf(this);
}
}
accept 里传入 this,让访问者拿到具体类型。
三、定义具体访问者
class ExtractTextVisitor implements Visitor {
public void visitPdf(PdfFile file) {
System.out.println("提取 PDF 文本:" + file.getPath());
}
public void visitWord(WordFile file) {
System.out.println("提取 Word 文本:" + file.getPath());
}
}
如果以后新增压缩操作,只需要新增 CompressVisitor,不必修改 PdfFile 和 WordFile。
四、常见误区与工程判断
代码实现中最容易漏的是:具体元素的 accept 不能写成统一的 visitor.visit(this),除非语言或接口设计支持对应重载并能正确分派。Java 的重载选择在编译期决定,访问者模式通常需要明确写出 visitPdf(this) 这类方法。
工程中如果访问者越来越多,可以按业务模块组织访问者,避免所有访问者堆在同一个包里。访问者模式解决操作扩展,但不解决代码组织混乱,需要配合包结构管理。
五、代码实现时最该注意的细节
Java 实现访问者模式时,最容易写错的是把分派逻辑写回 visitor 里,例如在 visit(Element e) 中用 instanceof 判断类型。这样虽然能跑,但本质上没有发挥访问者模式的多态分派优势。更规范的写法是 Visitor 接口为每个具体元素提供明确的 visitXxx 方法,具体元素在 accept 中传入 this。
第二个细节是 Visitor 接口的稳定性。只要新增元素类型,Visitor 接口就要变化,所有实现类都要补方法。因此在真实项目里,访问者适合元素类型经过建模后比较稳定的结构。如果元素类型还在频繁探索期,过早上访问者会导致大量联动修改。
第三个细节是返回值和上下文。示例里常写成 void visitXxx,但工程里经常需要返回结果或累计状态。可以让 visitor 内部保存累计值,也可以把 visit 设计成带返回值的泛型接口。选择哪种方式取决于操作是否有共享状态、是否需要线程安全、是否希望 visitor 可复用。
六、用工程约束检验答案
Java 实现的验收点不是类名齐全,而是 Employee.accept 必须调用 visitor.visit(this),Department.accept 也必须传自己的 this。用 2 种元素和 2 个访问者测试,应覆盖 4 个“访问者 × 元素”组合,才能证明分派完整。
| 检查项 | 核心判断 | 工程含义 |
|---|---|---|
| Element.accept | 只接收 Visitor | 不包含报表或审计细节 |
| Visitor 重载 | 参数使用具体元素类型 | 获得编译期类型检查 |
| ConcreteVisitor | 实现完整 visit 集合 | 一种操作集中在一处 |
把关键关系压缩成一条可复述的路径:
2 elements x 2 visitors = 4 checks
Employee -> SalaryVisitor / AuditVisitor
Department -> SalaryVisitor / AuditVisitor
每条路径断言独立结果
若 accept 中出现一长串 instanceof,说明示例没有真正利用访问者的分派结构。
落地前可以再按下面 3 步复核:
- 先说明“Element.accept”的核心机制:只接收 Visitor;再交代边界:不包含报表或审计细节。
- 接着分析“Visitor 重载”:参数使用具体元素类型;不能遗漏对应代价或结果:获得编译期类型检查。
- 最后用“ConcreteVisitor”检查方案:实现完整 visit 集合;验收时确认一种操作集中在一处。
这三项构成完整判断链:先讲清Element.accept,再说明Visitor 重载,最后用ConcreteVisitor检验实现是否越界。
面试中若能给出违反“一种操作集中在一处”的反例,再说明修正办法,答案就从模式定义落到了可验证的工程决策。
七、常见误区与追问
- 误区:只看到“Element.accept”就认为方案成立。 必须同时说明核心机制“只接收 Visitor”和工程边界“不包含报表或审计细节”。
- 误区:把“Visitor 重载”当成无条件结论。 只有在“参数使用具体元素类型”成立时,才能据此讨论“获得编译期类型检查”。
- 追问:Visitor 接口能否用泛型返回值? 可以设计
Visitor<R>,但不同 visit 返回类型需统一为 R。 - 追问:accept 能否返回结果? 可以返回泛型结果,也可让访问者累积结果;应按调用语义选择。
- 追问:访问者实例能否做单例? 无状态访问者可以,有累计字段的访问者则要考虑并发和污染。
- 追问:是否必须把所有类设为 public? 不必,可利用包可见性收紧 Memento 类似的协作边界。
- 追问:怎样测试新增元素的影响? 编译器会提示所有未实现的新 visit 方法,再为每个访问者补行为测试。
八、加强记忆
记忆时抓住这条主线:Java 实现访问者要有 Visitor 和 Element 两类接口;元素在 accept 中调用具体 visit 方法;新增操作就是新增 ConcreteVisitor;Java 重载分派细节要特别注意。面试回答先给出模式意图,再用调用链或数据流说明角色协作,最后主动交代适用边界与工程代价。