设计模式 23:组合商品如何统一报价
原来商品单价与平铺套餐分别走两个报价入口。增加“套餐里还能放套餐”之后,调用方必须先辨别是单品、列表还是嵌套列表,并自己递归。若所有节点都支持同一个 price(),谁负责保证空套餐与错误价格的边界?
单品和组合遵守同一读协议
labs/23 中的旧单品 200 分、平铺套餐 200+300=500 分在三种方案中保持不变。After 是受限的节点协议,Leaf.price() 返回非负单价,Group.price() 对子节点递归求和;空组合按本章约定返回 0。List.copyOf 固定孩子引用列表,防止调用者直接把节点塞进既有组合;加法用 Math.addExact 避免整数溢出静默回绕。
1 | |
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。没有数据库读写、环形图或大规模性能实测。
- 新增“组合商品内可有折扣节点”的需求,先让单品和嵌套旧合同通过,再设计折扣节点的顺序与空组合行为并运行新增断言;不要让负价抵消总额。
- 把本章输入限制为始终平铺,移除组合节点,仅用
Alternative;写测试检验结果一致并指出失去的嵌套表达能力。
参考资料
- GoF 原书公开图书馆 PDF,4.3 Composite,目录页标注起始页 183: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/23/。
上一节:22 两条变化维度如何分开;下一节:24 包装行为的先后顺序。
All articles on this blog are licensed under CC BY-NC-SA 4.0 unless otherwise stated.






