原来商品单价与平铺套餐分别走两个报价入口。增加“套餐里还能放套餐”之后,调用方必须先辨别是单品、列表还是嵌套列表,并自己递归。若所有节点都支持同一个 price(),谁负责保证空套餐与错误价格的边界?

单品和组合遵守同一读协议

labs/23 中的旧单品 200 分、平铺套餐 200+300=500 分在三种方案中保持不变。After 是受限的节点协议,Leaf.price() 返回非负单价,Group.price() 对子节点递归求和;空组合按本章约定返回 0。List.copyOf 固定孩子引用列表,防止调用者直接把节点塞进既有组合;加法用 Math.addExact 避免整数溢出静默回绕。

1
2
3
After nested = new Group(List.of(
new Leaf(200),
new Group(List.of(new Leaf(300)))));

Before 的两个单独入口没有对嵌套套餐定义方法;不把“旧版没实现”伪造成运行时出错。Alternative 先把所有单品展平再求和,若需求从来只需总价、不要求保留组边界,这比引入树节点协议简单。Composite 适用于调用者需要统一处理叶与容器且业务结构确实是树的情况;任意图有循环,需要另外定义环检测和共享节点计价语义,不应拿本例的不可变层级构造当作图算法。Group 的孩子列表不可修改不等于叶价格有持久化一致性;例子里 Leaf 是只读 record。

方案 单品与平铺套餐 嵌套、空组合、错误价格
Before 两种入口分别处理 无嵌套入口
After 同一 price() 调用 嵌套递归、空组合为 0;负数和溢出拒绝
Alternative 接受已展平单品清单 空列表 0;谁来展平由调用者承担
flowchart LR
    Caller[报价调用方] --> Component[After 接口]
    Leaf[Leaf] -.实现.-> Component
    Group[Group] -.实现.-> Component
    Group --> Children[children 快照]
    Children --> Component

实验与练习

使用 ./mvnw -B -ntp -pl labs/23 -am test 和 ./mvnw -B -ntp verify;实际命令、环境、输出与退出码在 examples/design-patterns/evidence/23/RUN.md。没有数据库读写、环形图或大规模性能实测。

  1. 新增“组合商品内可有折扣节点”的需求,先让单品和嵌套旧合同通过,再设计折扣节点的顺序与空组合行为并运行新增断言;不要让负价抵消总额。
  2. 把本章输入限制为始终平铺,移除组合节点,仅用 Alternative;写测试检验结果一致并指出失去的嵌套表达能力。

参考资料

上一节:22 两条变化维度如何分开;下一节:24 包装行为的先后顺序。