示例工程教程【免费下载链接】java-design-patternsDesign patterns implemented in Java项目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns点击查看免费下载Acyclic Visitor无环访问者是一种行为型设计模式它在经典 GoF Visitor 模式的基础上将访问者基类退化为空接口从而在不修改既有类层次结构的前提下为其追加新功能同时避免 GoF Visitor 模式固有的依赖环问题。本文以 java-design-patterns 仓库中的 acyclic-visitor 模块为核心结合其 阿拉伯语文档 与完整源码逐层拆解该模式的设计动机、类层次结构、运行流程、适用场景与代价权衡读完即可在自有类层次中复现并正确选用该模式。一、模式意图在不触碰类层次与不产生依赖环之间取得平衡按照模式文档localization/ar/acyclic-visitor/README.md的表述Acyclic Visitor 的核心目标是允许向已有的类层次结构class hierarchy添加新功能既不修改、不影响该层次本身也不会产生经典 GoF Visitor 模式中那种令人困扰的循环依赖dependency cycles。用更通俗的话说Acyclic Visitor 允许在不修改既有类层次的前提下向该层次追加新的功能。该模式最早由 Robert C. MartinUncle Bob提出WikiWikiWeb 上对其的定义与此一致它允许新函数被添加到既有类层次中同时不影响该层次也不会制造 GoF Visitor 模式固有的循环依赖。其关键技巧是让 Visitor 基类退化degenerate为空接口——基类不再声明任何 visit 方法从而切断访问者接口必须反向依赖所有被访问元素类型的强耦合。二、真实世界场景调制解调器Modem层次与外部配置算法文档给出的现实类比非常直观我们有一个 Modem调制解调器类层次其中的各型号调制解调器需要被某种外部算法按过滤条件访问——例如判断该调制解调器是兼容 Unix 还是兼容 DOS。在这个场景中针对 Unix 配置、针对 DOS 配置这类算法并不天然属于 Modem 层次本身却又必须针对不同型号执行不同处理。这正是 Visitor 类模式最典型的需求形态操作operation属于层次之外却按元素类型分派dispatch。三、模块源码结构总览在仓库中该模式位于 acyclic-visitor 目录其主源码src/main/java/com/iluwatar/acyclicvisitor/共包含 9 个 Java 文件分工如下文件角色Modem.java被访问类层次的根Zoom.java具体被访问元素Zoom 调制解调器Hayes.java具体被访问元素Hayes 调制解调器ModemVisitor.java访问者基接口退化空接口ZoomVisitor.javaZoom 专属访问者接口HayesVisitor.javaHayes 专属访问者接口AllModemVisitor.java聚合所有子访问者接口的便捷接口ConfigureForDosVisitor.java具体访问者DOS 配置器ConfigureForUnixVisitor.java具体访问者Unix 配置器只实现 Zoom 的 visitApp.java程序入口演示访问过程对应的单元测试位于 src/test/java/com/iluwatar/acyclicvisitor包括ZoomTest、HayesTest与AppTest。UML 结构图源文件为 acyclic-visitor.urm.puml。四、被访问层次Modem 根与具体型号文档给出的层次骨架如下public abstract class Modem { public abstract void accept(ModemVisitor modemVisitor); } public class Zoom extends Modem { ... Override public void accept(ModemVisitor modemVisitor) { if (modemVisitor instanceof ZoomVisitor) { ((ZoomVisitor) modemVisitor).visit(this); } else { LOGGER.info(Only ZoomVisitor is allowed to visit Zoom modem); } } } public class Hayes extends Modem { ... Override public void accept(ModemVisitor modemVisitor) { if (modemVisitor instanceof HayesVisitor) { ((HayesVisitor) modemVisitor).visit(this); } else { LOGGER.info(Only HayesVisitor is allowed to visit Hayes modem); } } }值得注意的源码级细节文档示例中使用abstract class Modem而当前仓库的实际实现已将层次根声明为接口。查看 Modem.java 可以看到其注释明确写着Modem abstract class. converted to an interfacepublic interface Modem { void accept(ModemVisitor modemVisitor); }两种写法表达的是同一设计意图——层次根只约定accept(ModemVisitor)契约。而 Zoom.java 与 Hayes.java 则各自实现accept其中关键逻辑是接受所有访问者accept的参数类型统一为根接口ModemVisitor只认可匹配自己类型的访问者通过instanceof判断访问者是否实现了与自身对应的子访问者接口不匹配时静默降级记录日志Only ZoomVisitor is allowed to visit Zoom modem等而不是抛出编译错误或运行异常。这里instanceof判断就是 Acyclic Visitor 与经典 Visitor 的分水岭经典 Visitor 依赖编译期的双重分派元素类型 × 访问者类型因此访问者接口必须声明对所有元素类型的 visit 方法而 Acyclic Visitor 把分派推迟到运行期由元素用instanceof自行筛选认识自己的访问者从而彻底解除访问者接口对元素层次的编译期依赖。五、访问者层次退化基接口 逐型号子接口紧接着引入ModemVisitor访问者层次public interface ModemVisitor { } public interface HayesVisitor extends ModemVisitor { void visit(Hayes hayes); } public interface ZoomVisitor extends ModemVisitor { void visit(Zoom zoom); } public interface AllModemVisitor extends ZoomVisitor, HayesVisitor { } public class ConfigureForDosVisitor implements AllModemVisitor { ... Override public void visit(Hayes hayes) { LOGGER.info(hayes used with Dos configurator.); } Override public void visit(Zoom zoom) { LOGGER.info(zoom used with Dos configurator.); } } public class ConfigureForUnixVisitor implements ZoomVisitor { ... Override public void visit(Zoom zoom) { LOGGER.info(zoom used with Unix configurator.); } }对照当前源码这一结构的语义逐层清晰ModemVisitor.java源码注释明确指出它不含任何 visit 方法因此它不依赖被访问的层次——这就是文档所称的退化的访问者基类degenerate base class for all visitors。也正是这个空接口让新增元素类型不需要改动任何既有访问者接口成为可能。ZoomVisitor.java、HayesVisitor.java每个元素型号声明自己专属的访问者接口各自携带visit(对应型号)方法。AllModemVisitor.javaextends ZoomVisitor, HayesVisitor提供便捷聚合——当某个访问者需要访问全部型号时直接实现该接口即可源码注释provides ease of use when a visitor needs to visit all modem types。ConfigureForDosVisitor.java实现AllModemVisitor同时提供 Hayes 与 Zoom 两种 visit 方法。ConfigureForUnixVisitor.java只实现ZoomVisitor。其源码注释点出了与传统 Visitor 的本质区别——不同于传统访问者模式本类可以有选择地只为部分型号实现 visit 方法unlike traditional visitor pattern, this class may selectively implement visit for other modems。这正是 Acyclic Visitor 的核心价值访问者可以只关心它感兴趣的型号而不必为所有型号实现空方法或抛异常。六、程序入口与完整运行输出最后在 App.java 的main中把访问者投入实战public static void main(String[] args) { var conUnix new ConfigureForUnixVisitor(); var conDos new ConfigureForDosVisitor(); var zoom new Zoom(); var hayes new Hayes(); hayes.accept(conDos); // Hayes modem with Dos configurator zoom.accept(conDos); // Zoom modem with Dos configurator hayes.accept(conUnix); // Hayes modem with Unix configurator zoom.accept(conUnix); // Zoom modem with Unix configurator }程序输出文档原文时间戳部分为运行环境信息// Hayes modem used with Dos configurator. // Zoom modem used with Dos configurator. // Only HayesVisitor is allowed to visit Hayes modem // Zoom modem used with Unix configurator.逐行解读这段输出可以直观看到分派逻辑hayes.accept(conDos)conDos是AllModemVisitor实现HayesVisitor故instanceof HayesVisitor成立 → Hayes 被 DOS 配置器访问zoom.accept(conDos)同理conDos实现ZoomVisitor→ Zoom 被 DOS 配置器访问hayes.accept(conUnix)conUnix只实现ZoomVisitor不满足instanceof HayesVisitor→ 输出Only HayesVisitor is allowed to visit Hayes modem操作被安全拒绝而不是编译失败或运行崩溃zoom.accept(conUnix)instanceof ZoomVisitor成立 → Zoom 被 Unix 配置器访问。七、类图与调用时序文档中给出了该模式的类图acyclic-visitor.png与 acyclic-visitor.urm.puml 描述的 UML 结构一致Modem在 puml 中仍以 abstract class 表达派生出Hayes、ZoomModemVisitor派生出HayesVisitor、ZoomVisitor二者再汇聚为AllModemVisitorConfigureForDosVisitor实现AllModemVisitorConfigureForUnixVisitor仅实现ZoomVisitor。Acyclic Visitor 模式类图Modem 层次与访问者层次的对应关系从调用时序acyclic-visitor-sequence-diagram.png可以看出完整的消息链调用方创建具体访问者 → 调用元素的accept(ModemVisitor)→ 元素内部通过instanceof校验访问者类型 → 校验通过则回调visitor.visit(this)完成双重分派。Acyclic Visitor 模式时序图accept 到 visit 的调用链八、适用场景何时选用 Acyclic Visitor按照文档的应用一节该模式适用于以下情形需要向既有类层次添加新功能且不希望改动或影响该层次本身存在作用于层次之上、但逻辑上不属于该层次的功能——例如文档列举的ConfigureForDOS、ConfigureForUnix、ConfigureForX这类配置器需要根据对象的具体类型执行差异极大的操作被访问的类层次预计会频繁扩展不断出现 Element 类的新衍生类Element 衍生类的重新编译recompilation、重新链接relinking、重新测试retesting或重新分发redistribution成本极高——此时任何一次新增元素导致所有访问者重编译都是不可接受的而 Acyclic Visitor 恰好避免这一点。最后一条是决策的关键如果你的元素类型变化频繁、而操作集相对稳定经典 Visitor 会迫使每加一个元素就要改所有访问者并整体重编译Acyclic Visitor 则让新增元素只影响其自身的 accept 方法及其专属访问者接口。九、后果与代价优点与缺陷文档的后果一节从正反两面给出了客观评估。优点好的方面层次之间不存在循环依赖no dependency cycles访问者基接口为空不反向引用任何元素类型新增访问者时无需重新编译所有访问者访问者各自只依赖自己关心的子接口层次新增成员不会导致既有访问者产生编译错误因为不存在必须为所有元素提供 visit 方法的编译期约束不匹配的情况在运行期被instanceof优雅拦截并记日志。缺点不好的方面违背里氏替换原则Liskov Substitution Principle元素对外宣称接受所有ModemVisitor实际却只认可与自身类型匹配的某一种访问者accept中instanceof判断后的拒绝分支行为的可替换性在运行期打了折扣必须维护一条与可访问层次成员一一对应的并行访问者接口层次每个元素型号都要配套一个专属访问者接口接口数量随元素数量线性增长。此外从实现角度还可以补充两点权衡由于分派发生在运行期instanceof链相比经典 Visitor 的编译期双重分派会带来轻微的类型检查开销同时若访问者类型判断分支增多维护成本也会上升。十、与经典 Visitor 模式的关系文档明确指出该模式与经典 Visitor 模式 紧密相关本模块可视为 Visitor 的一个变体Acyclic Visitor 通过把访问者基类退化为空接口消除了 GoF Visitor 中访问者接口依赖全部元素类型这一环从而把元素—访问者依赖图变成无环结构。仓库中的 visitor 模块保留了 GoF 原始形态二者对照阅读可以清晰看到去环前后在接口设计、分派方式与扩展成本上的差异。十一、源码级验证单元测试如何印证分派逻辑仓库提供的测试从行为层面印证了上述设计。以 ZoomTest.java 为例它使用 Mockito 构造ConfigureForDosVisitor的 mock 对象验证accept后确实触发了对应的visit调用verify并覆盖不匹配访问者被拒绝并记日志的路径HayesTest.java 与 AppTest.java 则分别验证 Hayes 的接受逻辑与整体运行流程。这些测试共同保证了匹配即访问、不匹配即拒绝这一核心语义在重构中不被破坏。十二、小结Acyclic Visitor 是对经典 Visitor 的一次精准手术用一个退化的空接口做访问者基类把编译期的全量分派降级为运行期的按需识别换取无依赖环、增量扩展、选择性实现三大能力。它的适用边界非常清晰——当你的元素层次变化频繁、重编译成本高、且访问者只需关心部分元素类型时它是比 GoF Visitor 更稳妥的选择但你也必须接受里氏替换原则的妥协与每元素一访问者接口的平行结构成本。本文所述原理与代码均可直接对照仓库 acyclic-visitor 模块的源码、测试与 英文原版文档 深入研读。赞分享示例工程教程【免费下载链接】java-design-patternsDesign patterns implemented in Java项目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns点击查看免费下载相关推荐Unity3DTraining 设计模式实战访问者模式Visitor Pattern原理与 C 双示例详解Unity3DTraining 设计模式实战访问者模式Visitor Pattern原理与 C 双示例详解 导读 本文围绕 DesignPatterns/示例工程HumanLayer Skills 中 response-template.md 如何定义 PR body 输出格式新手完整指南HumanLayer Skills 中 response template.md 如何定义 PR body 输出格式新手完整指南 在 HumanLayer S示例工程教程如何快速掌握PHP访问者模式Visitor设计模式完整指南如何快速掌握PHP访问者模式Visitor设计模式完整指南 在PHP开发中设计模式是提升代码质量和可维护性的关键。访问者模式Visitor Pattern示例工程教程上一篇MathModelAgent把 3 天数模竞赛压缩到 1 小时自动生成可提交论文的 AI 建模 Agent下一篇Hello-Python项目中的网络平台资金操作问题技术解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考