设计模式 37:新操作与新节点哪个更贵
报表节点只有单品与组合时,导出文本已经可用。新需求是在不改已有导出代码的前提下校验“名称不能为空且金额不能为负”。若未来还要新增收费节点,原来的取舍会不会反过来?Visitor 的价值取决于“对象种类稳定、操作常变”是否真的成立。
新操作容易;新节点会扩散
labs/37 的 Before.export 用 instanceof 分支递归导出 [paper:200,[pen:100]]。Node.accept(visitor) 则让实际 Item 或 Group 类型决定调用 visitItem / visitGroup,访问者再决定返回内容;Export 复现旧文本,新增 Validate 不必修改这两种节点的 accept 代码。测试断言负价节点导出仍有文本,却在新校验中返回 false;空组合导出 []、校验为 true。导出和校验是两个不同合同,不能把“能导出”误认为“业务值有效”。
1 | |
为了展示反方向代价,实验又加入 Fee 节点。需要改 Node permits、新增 Fee.accept、扩展 Visitor.visitFee,并给 Export 与 Validate 都补处理;旧 Before 的手写类型分支及 Alternative 的直接递归函数也要补 Fee 情况。JUnit 同时断言收费节点 fee:30 可导出、30 分合法、负数不合法,旧单品与组合断言不变。这些是本仓库具体改动位置,不能抽象成所有 Visitor 都更易扩展。
| 变化需求 | Visitor 版改动 | 小树直接分支 |
|---|---|---|
| 新增校验操作 | 新增一个实现 Visitor 的类 |
新增一套递归函数 |
新增 Fee 节点 |
节点接口及每个访问者都改 | 各类型分支改 |
若节点频繁变化、操作只有一个,Alternative 的直接递归更容易维护。Visitor 适合封闭且相对稳定的节点集合、操作种类经常增加;这里的语法树只遍历固定的教学数据,不进行任意代码执行。
flowchart LR
Caller[操作调用方] --> Node[Node.accept]
Item[Item / Fee] -.实现.-> Node
Group[Group] -.实现.-> Node
Group --> Children[子 Node]
Node --> Visitor[Visitor.visit]
Export[Export / Validate] -.实现.-> Visitor
验证与练习
运行 ./mvnw -B -ntp -pl labs/37 -am test 和累计 ./mvnw -B -ntp verify,环境、运行次数、退出码及原始输出见 examples/design-patterns/evidence/37/RUN.md。新增操作和节点分别有断言。
- 新增“计算所有合法节点的合计金额”的操作,先为已有导出/校验写回归,再实现一个访问者并覆盖空组合、负数和
Fee节点;记录所改的节点文件。 - 新增第四种节点
Note,不参与金额,但必须导出。先设计新节点的导出/校验断言,记录更新每个访问者所需修改,比较移除 Visitor 后的直接方案。
参考资料
- GoF 原书公开图书馆 PDF,5.11 Visitor,目录页标注起始页 366:https://cpcc.chd.gov.in/Content/PDFs/w8UkV3tWNyEtsbUZSWJ7fVhuB9A3tsGYdm4w6VGzg2wUTNFYikqnvvFbbkiW2zmfspPEghd7QTamiMby3lVIBemrhdVWwt6rOQnm.pdf 。
- Vlissides/Schmidt 公开课件
An Introduction to Design Patterns:https://www.dre.vanderbilt.edu/~schmidt/PDF/GoF.pdf 。 - 本篇来源边界:
writing-plans/design-patterns/SOURCES.md。 - 实验代码:
examples/design-patterns/labs/37/。
上一节:36 快照如何限制内存与别名;下一节:38 折扣表达式需要语法树吗。






