设计模式 E02:record、sealed 类型与模式匹配
第 37 篇用 Visitor 处理“节点类型稳定、操作会增加”的场景。Java 21 的 record、sealed 和 switch 模式匹配把这类模型的另一种写法放进语言里:节点可以是数据载体,类型集合可以被封闭,switch 可以在编译期检查是否覆盖所有已知类型。
record 和 sealed 改变的是数据边界
Oracle JDK 21 文档说明,record 会为组件生成字段、访问器、规范构造器、equals、hashCode 和 toString;record 字段是 final,适合作为简单数据载体。sealed class/interface 则限制哪些类可以继承或实现它。labs/E02 里,Line 只允许 Product 和 Bundle 两种实现,Product 校验 SKU 和金额,Bundle 保存子节点副本。
1 | |
这段只截取 Product,完整实验还包含复制子列表的 Bundle。record 组件字段是 final,但这只是浅层不可变;如果组件本身是可变集合,仍要像 Bundle 那样在构造时 List.copyOf。这不是把 Visitor 删除,而是让“类型集合稳定”落在语言结构里。visitorAndSwitchKeepTheSameResultForStableNodeTypes 证明 Visitor 与 switch 对同一棵订单行树都算出 1250 分。
当节点集合真的稳定时,sealed 层次能把扩展边界交给编译器;当外部插件要继续增加节点时,sealed 会变成不合适的门。
新增操作时 switch 很直接
新增“列出 SKU”这个操作,switch 写成一个旁路函数即可:
1 | |
newOperationCanBeAddedNextToSwitchWithoutChangingRecords 断言结果为 book,pen。这正好命中 Visitor 的经典取舍:新增操作时,Visitor 需要新增一个 visitor 实现;switch 只需要新增函数。二者都不需要改 Product 和 Bundle。
flowchart TB
Line[Line sealed层次] --> Product[Product]
Line --> Bundle[Bundle]
Product --> Accept["accept(visitor)"]
Bundle --> Accept
Accept --> Visitor[Visitor求金额]
Line --> Switch[switch求金额/列SKU]
新增类型时让编译器说话
本篇没有用文字推断“会编译失败”,而是在测试里调用 JavaCompiler 编译负例。新增 Gift 但 switch 只处理 Product 时,JDK 21 诊断包含 switch 表达式未覆盖所有输入值;新增 Gift.accept(visitor) 但 Visitor 只有 visit(Product) 时,诊断包含 Gift 无法匹配现有 visit 方法。
这两个负例说明:sealed + switch 和 Visitor 都能在新增节点时逼迫调用方补齐操作,只是失败位置不同。switch 失败在穷尽性检查,Visitor 失败在新节点接入访问者时找不到对应方法。对稳定类型集合,switch 更短;对跨模块开放类型集合,Visitor 或普通多态更自然。
| 变化 | Visitor | sealed + switch |
|---|---|---|
| 新增操作 | 新增 visitor 实现 | 新增函数 |
| 新增节点 | 改 visitor 接口和实现 | 改 sealed permits 与所有穷尽 switch |
| 数据载体 | 可用普通类或 record | record 更简短 |
Visitor 与模式匹配解决的是同一个二维变化问题:类型轴和操作轴。不要只比较语法行数,要先判断哪根轴更常变。
验证与练习
运行 ./mvnw -B -ntp -pl labs/E02 -am test;SealedSwitchVisitorTest 通过 3 个测试,其中包含两个负向编译诊断。原始日志在 examples/design-patterns/evidence/E02/targeted.stdout.txt。
- 新增
DiscountLine节点,先让现有 switch 和 Visitor 负向测试失败,再补齐金额和 SKU 操作;记录要改的文件数量。 - 新增“统计叶子节点数量”操作,分别用 Visitor 和 switch 写一版。比较新增操作时哪一版更短,以及哪一版更容易复用已有递归逻辑。
参考资料:
- Oracle Java SE 21 Language Guide:Record Classes。
- Oracle Java SE 21 Language Guide:Sealed Classes and Interfaces。
- Oracle Java SE 21 Language Guide:Pattern Matching for switch Expressions and Statements。
上一节:E01 高阶函数替代了什么;下一节:E03 默认行为和规则组合。






