设计模式 24:包装行为的先后顺序
报价先打九折再收 100 分服务费:1000 分变成 1000 分。如果合同改成“服务费也打九折”,结果是 990 分。只看最终类图很难发现错误,必须看到谁包装谁、哪个金额先进入折扣。若内部报价抛异常或持有待关闭资源,外层是否会吞掉错误或遗漏关闭?
顺序是计算合同的一部分
Before.quote(1000) 固定为“先折扣后加费”。After 让 Discount 与 Fee 都实现 quote() 和 close(),并持有同一种接口:外层 Fee(new Discount(new Base(1000))) 得 1000;外层 Discount(new Fee(new Base(1000))) 得 990。Alternative 直接在函数里按布尔参数选执行次序;只有两种固定方案、无需运行时组合对象时更加直白。
1 | |
同接口叠加新行为是 Decorator 的意图;不是 Adapter 的协议转换、Proxy 的访问控制、Bridge 的双轴扩展或 Facade 的统一入口。这里包装前的负金额从 Base.quote() 抛出,外层不捕获,所以错误向调用者传播。两层包装的 close() 沿着链调用一次,测试用计数器验证底层资源只关闭一次。这仅验证此处单次关闭的案例,不证明重复关闭、关闭失败后恢复或实际 IO 资源可用。
| 方案 | 1000 分先折扣再收费 | 先收费再折扣 |
|---|---|---|
Before |
1000 | 旧方法没有参数可选此规则 |
After |
1000 | 990,改变包装顺序 |
Alternative |
1000 | 990,改变布尔参数 |
若两种操作始终一起发布而且先后固定,直接算式更短;当调用者确实要自由组合多个附加职责,包装才比多组固定分支有价值。不要在装饰器里隐式吞掉报价失败;否则 0 分结果可能被误写成成功订单。没有线程竞争、银行舍入、税法或真实资源释放语义。
flowchart LR
Caller[报价调用方] --> Outer[Fee 或 Discount 外层]
Outer --> Inner[After wrapped]
Inner --> Base[Base]
Outer -.close 向内传递.-> Inner
实验与练习
执行 ./mvnw -B -ntp -pl labs/24 -am test 和累计 ./mvnw -B -ntp verify;本地环境、第一次编译失败与更正后实际通过的原始输出均见 examples/design-patterns/evidence/24/RUN.md。测试断言两种顺序、异常传播与一次资源关闭,不是只打印金额。
- 新增“在折扣之前先加固定包装费 50 分”,为旧计算结果和至少两种新次序写失败断言,再重新组合包装器并复跑旧合同。
- 模拟底层
close()抛异常,写断言规定哪个错误向上传递;若不需要动态组合,改写成带明确顺序的普通函数并比较复杂度。
参考资料
- GoF 原书公开图书馆 PDF,4.4 Decorator,目录页标注起始页 196:https://cpcc.chd.gov.in/Content/PDFs/w8UkV3tWNyEtsbUZSWJ7fVhuB9A3tsGYdm4w6VGzg2wUTNFYikqnvvFbbkiW2zmfspPEghd7QTamiMby3lVIBemrhdVWwt6rOQnm.pdf 。
- Java SE 21
FilterInputStreamJavadoc:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/FilterInputStream.html 。 - Java SE 21
BufferedInputStreamJavadoc:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/BufferedInputStream.html 。 - 实验代码:
examples/design-patterns/labs/24/。
上一节:23 组合商品如何统一报价;下一节:25 用例入口如何暴露失败。






