Java 设计模式之 Visitor(访问者)模式:java-design-patterns 仓库源码级实战解析

发布时间:2026/10/14 2:02:00
Java 设计模式之 Visitor(访问者)模式:java-design-patterns 仓库源码级实战解析
示例工程教程【免费下载链接】java-design-patternsDesign patterns implemented in Java项目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns点击查看免费下载导读本文以 java-design-patterns 仓库的visitor模块visitor/README.md为核心系统讲解 GoF 经典行为型设计模式 —— Visitor访问者模式的意图、结构、实现原理与使用边界。文章以仓库中完整的军队层级示例代码为骨架逐行剖析Unit元素层级与UnitVisitor访问者的协作机制并结合visitor模块下的源码、PlantUML 模型与 JUnit 测试用例进行纵深验证。读完本文你将掌握如何在不修改既有元素类的前提下为稳定的对象结构持续叠加新操作以及如何通过双分派Double Dispatch在 Java 中实现这一模式。一、模式意图IntentVisitor 模式的意图非常明确在对象结构的元素上表示一个要执行的操作。Visitor 允许你在不改变操作所作用的元素类的前提下定义新的操作。换句话说当一组对象例如树形结构中的各个节点的类结构保持稳定而你需要不断在其上追加新的操作时Visitor 模式是解耦算法与对象结构的标准答案。它把对每个元素做什么的逻辑从元素类中剥离出来集中放入独立的访问者类中从而避免在元素类中堆积越来越多、互不相关的行为代码。该模式在 visitor/README.md 中被归类为 Behavioral行为型设计模式并标注了 Decoupling解耦、Extensibility可扩展性、Gang of FourGoF 经典模式、Object composition对象组合、Polymorphism多态等标签。二、真实世界类比与通俗解释1. 博物馆导览系统原文档给出了一个非常形象的现实类比想象一座博物馆游客可以参加导览团了解不同类型的展品例如绘画、雕塑和历史文物。每种展品都需要不同的讲解由专门的导游提供。在这个场景中展品绘画、雕塑、文物对应 Visitor 模式中的元素Element它们自身结构固定不变导游Tour Guide对应访问者Visitor不同的导游携带不同的讲解能力博物馆的展品布局不需要为每一位新导游而改动只要新增一位导游就能为所有展品提供一种全新的讲解视角操作。这与 Visitor 模式的核心思想完全一致操作与对象分离操作可扩展对象结构封闭。2. 一句话理解Visitor 模式定义了可作用于多种数据结构节点上的操作从而增强 Java 应用的可扩展性。用 OOP 术语再精炼一层Wikipedia 的经典定义在面向对象编程中Visitor 模式是一种将算法与其作用的对象结构分离的方式其直接成果是无需修改既有结构即可为其添加新操作。三、模式结构与角色1. 类图下图展示了本模块的完整类结构对应 visitor/etc/visitor.pngPlantUML 源文件见 visitor/etc/visitor.urm.puml2. 时序图原文档内置的时序图visitor/etc/visitor-sequence-diagram.png展示了一次典型的双分派调用流程3. 模式角色一览结合 visitor/etc/visitor.urm.puml 中定义的类模型本模块中的角色对应关系如下模式角色本模块实现类职责Element抽象元素abstract class Unit声明accept(UnitVisitor)方法持有子节点数组ConcreteElement具体元素Commander、Sergeant、Soldier实现accept先调用visitor.visit(this)再递归转发给子节点Visitor抽象访问者interface UnitVisitor为每种元素类型声明一个重载的visit(...)方法ConcreteVisitor具体访问者CommanderVisitor、SergeantVisitor、SoldierVisitor只关心自己感兴趣的节点类型其余类型的方法体为空ObjectStructure对象结构由Commander递归组合的树形结构提供元素集合与遍历入口四、源码级编程示例军队层级遍历原文档选用了一个军队编制树作为示例1 名指挥官Commander统辖 2 名军士Sergeant每名军士下辖 3 名士兵Soldier。下面我们结合visitor模块的真实源码逐步拆解。1. 元素基类 Unit 与访问者接口 UnitVisitor首先是节点基类 Unit.java它用一个Unit[]数组持有子节点并提供默认的accept遍历逻辑public abstract class Unit { private final Unit[] children; public Unit(Unit... children) { this.children children; } /** Accept visitor. */ public void accept(UnitVisitor visitor) { Arrays.stream(children).forEach(child - child.accept(visitor)); } }可以看到基类Unit.accept的默认实现就是把访问者转发给所有子节点——这为整棵树的递归遍历提供了基础设施。叶子节点没有子节点的节点传入空的Unit... children即可。访问者接口 UnitVisitor.java 为每一种具体元素类型声明一个重载方法public interface UnitVisitor { void visit(Soldier soldier); void visit(Sergeant sergeant); void visit(Commander commander); }接口中方法的数量决定了访问者能够认得出哪些节点类型这是双分派得以成立的关键。2. 具体元素Commander / Sergeant / Soldier三个具体元素类结构几乎一致以 Commander.java 为例public class Commander extends Unit { public Commander(Unit... children) { super(children); } Override public void accept(UnitVisitor visitor) { visitor.visit(this); super.accept(visitor); } Override public String toString() { return commander; } }SergeantSergeant.java与SoldierSoldier.java是同一套路只是toString()分别返回sergeant与soldier。这里有一个非常关键的细节每个具体元素重写accept时调用的是visitor.visit(this)而不是把this转为基类再调用。由于visit是重载方法visit(Soldier)/visit(Sergeant)/visit(Commander)而this的编译期静态类型就是本类因此 JVM 在运行时能精确地分派到与元素实际类型匹配的visit重载——这正是双分派的第一个分派点元素 → 正确的visit方法。随后调用super.accept(visitor)把访问者继续下发给子节点实现深度优先的前序遍历。3. 具体访问者三个 Visitor三个具体访问者分别只对一类节点打招呼其余方法留空CommanderVisitor.java仅对Commander输出Good to see you {}SergeantVisitor.java仅对Sergeant输出Hello {}SoldierVisitor.java仅对Soldier输出Greetings {}。以 SoldierVisitor.java 为例Slf4j public class SoldierVisitor implements UnitVisitor { Override public void visit(Soldier soldier) { LOGGER.info(Greetings {}, soldier); } Override public void visit(Sergeant sergeant) { // Do nothing } Override public void visit(Commander commander) { // Do nothing } }三个访问者类都使用了 Lombok 的Slf4j注解注入 SLF4J 日志记录器依赖声明见 visitor/pom.xml日志输出通过LOGGER.info(... {}, unit)完成{}占位符会调用对应元素的toString()。4. 组装、运行与程序输出入口类 App.javamain方法位于 App.java#L42-L51组装出完整的军队树并依次派发三种访问者public static void main(String[] args) { var commander new Commander( new Sergeant(new Soldier(), new Soldier(), new Soldier()), new Sergeant(new Soldier(), new Soldier(), new Soldier())); commander.accept(new SoldierVisitor()); commander.accept(new SergeantVisitor()); commander.accept(new CommanderVisitor()); }树形结构为Commander ├── Sergeant │ ├── Soldier │ ├── Soldier │ └── Soldier └── Sergeant ├── Soldier ├── Soldier └── Soldier程序输出摘自 visitor/README.md14:58:06.115 [main] INFO com.iluwatar.visitor.SoldierVisitor -- Greetings soldier 14:58:06.118 [main] INFO com.iluwatar.visitor.SoldierVisitor -- Greetings soldier 14:58:06.118 [main] INFO com.iluwatar.visitor.SoldierVisitor -- Greetings soldier 14:58:06.118 [main] INFO com.iluwatar.visitor.SoldierVisitor -- Greetings soldier 14:58:06.118 [main] INFO com.iluwatar.visitor.SoldierVisitor -- Greetings soldier 14:58:06.118 [main] INFO com.iluwatar.visitor.SoldierVisitor -- Greetings soldier 14:58:06.118 [main] INFO com.iluwatar.visitor.SergeantVisitor -- Hello sergeant 14:58:06.118 [main] INFO com.iluwatar.visitor.SergeantVisitor -- Hello sergeant 14:58:06.118 [main] INFO com.iluwatar.visitor.CommanderVisitor -- Good to see you commander输出数量与顺序是完全可推导的SoldierVisitor只对士兵感兴趣深度优先遍历完整棵树后恰好命中 6 名士兵SergeantVisitor命中 2 名军士CommanderVisitor命中 1 名指挥官。日志中的时间戳14:58:06.115等是运行时刻的真实记录读者自行运行时会得到类似但时间不同的输出。五、双分派与遍历顺序的底层原理Visitor 模式之所以能在 Java 中成立靠的是accept visit 两次虚方法调用Double Dispatch第一次分派调用方对元素调用element.accept(visitor)。由于accept是虚方法运行时会进入与该元素实际类型对应的重写版本例如Commander.accept。第二次分派在accept内部执行visitor.visit(this)。this的静态类型决定了调用的是visit(Commander)还是visit(Soldier)重载从而反向选中最匹配的访问者方法。因此一次访问等于元素类型 × 访问者类型 的二维组合分派这正是 Java 单分派语言实现双分派语义的标准手段。关于遍历顺序从 Commander.java#L40-L43 与 Unit.java#L39-L41 的源码可以看出每个非叶子节点总是先访问自身visit(this)再通过 super.accept 递归遍历所有子节点即典型的前序深度优先遍历。同时由于每个节点都会把访问者无条件传给所有子节点无论访问者是否对该节点感兴趣整棵树都会被完整走一遍——这也是为什么三类访问者运行后输出的日志数量分别是 6、2、1 而不是更少。六、测试验证从测试用例反推契约visitor模块的测试充分印证了上述行为并可作为行为契约的自动化保障。1. 元素侧测试UnitTest 体系抽象基类 UnitTest.java 验证了accept的向下转发契约它构造 5 个 Mockito mock 子节点对被测单位调用unit.accept(visitor)然后断言每个子节点都收到了accept(visitor)调用verify(child).accept(eq(visitor))访问者恰好收到一次与自身类型匹配的visit调用。三个具体测试类分别只关心自己的类型CommanderTest.javaverify(mockedVisitor).visit(eq(unit))SergeantTest.java验证visit(Sergeant)被调用SoldierTest.java验证visit(Soldier)被调用。这组测试直接证明了元素通过accept触发与自己实际类型匹配的visit重载这一双分派核心。2. 访问者侧测试VisitorTest 体系抽象基类 VisitorTest.java 使用自定义的InMemoryAppender一个继承 logbackAppenderBase的内存日志收集器捕获 SLF4J 日志并针对每种节点类型断言访问者的响应消息CommanderVisitorTest.java期望Good to see you commander其余两个类型期望null无输出SergeantVisitorTest.java期望Hello sergeantSoldierVisitorTest.java期望Greetings soldier。3. 应用入口测试AppTest.java 使用assertDoesNotThrow(() - App.main(new String[] {}))验证整个遍历流程可以无异常地完整执行。七、构建与运行visitor是一个标准的 Maven 子模块其 visitor/pom.xml 继承仓库根 pom.xml依赖包括slf4j-api与logback-classic日志Slf4j注解依赖它们junit-jupiter-engine与mockito-core测试scope 为test。该模块还通过maven-assembly-plugin将 App.java 配置为可执行 JAR 的mainClass。仓库根目录提供了 Maven Wrappermvnw、mvnw.cmd因此可以在仓库根目录执行# 运行 visitor 模块的全部单元测试 ./mvnw test -pl visitor # 打包assembly 插件会将 com.iluwatar.visitor.App 写入可执行 JAR 清单 ./mvnw package -pl visitor之后运行主类com.iluwatar.visitor.App即可在控制台看到与上文一致的三段问候日志。八、何时使用 Visitor 模式根据原文档以下场景适合引入 Visitor 模式需要对一组相似对象高效执行批量操作而又不希望修改这些对象类、避免让操作污染元素类时对象类结构保持稳定但你需要在结构之上不断执行新的操作时元素类型集合固定不变只有操作需要持续扩展时。用一句口诀概括类不变操作变用 Visitor操作不变类变慎用 Visitor。九、真实世界的典型应用Visitor 模式在生产代码中非常常见原文档列举了如下经典场景编译器设计用于实现 pretty printing美化打印、语义检查semantic checks等操作——同一棵语法树可以被多个编译器阶段访问抽象语法树AST处理对 AST 的不同节点类型执行求值、类型检查、代码生成等操作文档结构处理例如对 HTML、XML 文档树执行序列化、校验、转换等操作JDK 标准库java.nio.file.FileVisitor配合Files.walkFileTree遍历文件树、javax.lang.model.element.Element与ElementVisitor、AnnotationValue与AnnotationValueVisitor等都是 Java 官方对 Visitor 模式的直接应用。十、优点与权衡主要优点简化新操作的添加新增一个操作只需新增一个 Visitor 实现类无需改动任何既有元素代码符合单一职责原则SRP把彼此相关的行为聚拢到同一个类中元素类不再为各种操作所累符合开闭原则OCP元素对修改关闭访问者对扩展开放。主要代价新增元素类型代价高一旦引入新的元素类型访问者接口及其全部具体实现都必须同步增加一个visit重载——这正是类稳定前提的意义所在可能引入循环依赖在复杂系统中访问者与元素类之间容易形成双向依赖可能破坏封装为了让访问者能干活元素类不得不暴露足够多的内部细节这可能打破封装边界。十一、与相关模式的协作Composite组合模式Visitor 通常与 Composite 配合使用——本示例的军队树本身就是一棵组合结构Visitor 负责在组合树上执行遍历操作Interpreter解释器模式可以用 Visitor 来实现解释器中的非终结符表达式non-terminal expressions把语法树上的解释动作外置Strategy策略模式可以认为 Visitor 是让策略Strategy能够作用于它原本并非为其设计的对象之上的一种方式。三者在本仓库中均有独立模块composite、interpreter、strategy读者可交叉阅读以加深理解。十二、参考资料本文核心内容继承自 visitor/README.md其知识背景来自以下经典著作《Design Patterns: Elements of Reusable Object-Oriented Software》GoF 四人组模式定义源头《Head First Design Patterns: Building Extensible and Maintainable Object-Oriented Software》《Java Design Patterns: A Hands-On Experience with Real-World Examples》《Refactoring to Patterns》若要在本仓库中动手实践请从 visitor/src/main/java/com/iluwatar/visitor 的 8 个类文件出发结合 visitor/etc/visitor.urm.puml 类模型与visitor/src/test下的 8 个测试类即可完整复现并验证本文描述的全部行为。赞分享示例工程教程【免费下载链接】java-design-patternsDesign patterns implemented in Java项目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns点击查看免费下载相关推荐Java 设计模式之 Visitor访问者模式java-design-patterns 仓库中的完整实现与实战解析Java 设计模式之 Visitor访问者模式java design patterns 仓库中的完整实现与实战解析 本篇以 java design pat示例工程教程Java Acyclic Visitor 模式全解析java-design-patterns 仓库中的无环访问者实现Java Acyclic Visitor 模式全解析java design patterns 仓库中的无环访问者实现 Acyclic Visitor无环访问示例工程教程Java 设计模式之观察者模式Observer Patternjava-design-patterns 源码级实战解析Java 设计模式之观察者模式Observer Patternjava design patterns 源码级实战解析 观察者模式Observer Pa示例工程教程上一篇突破像素限制Slint软件渲染器中大字体渲染优化指南下一篇终极指南攻克Intel RealSense Android Wrapper构建难题从环境配置到调试全流程解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考