设计模式 25:用例入口如何暴露失败
下单先报价、再预留库存、最后模拟扣款。旧调用者只拿到一个布尔值:缺货是 false,扣款被拒也同样是 false。更危险的是扣款失败前库存已经减了 1。给这一串调用加一个简单入口,能否让错误变得可辨识?会不会误让读者以为它保证了事务回滚?
收敛入口,不隐瞒局部副作用
labs/25 的 Before.checkout 直接依次调用 Quote.price、Stock.reserve、Payment.charge。After 把协作收在 checkout(base) 一处,返回 Result(Status, cents):成功、NO_STOCK、PAYMENT_FAILED 是三种可区分状态。报价 1000 分得 900 分的旧成功合同不变;0 分报价在预留之前就抛错,不会扣库存。扣款失败时测试明确断言 PAYMENT_FAILED 且余额从 1 变为 0:入口没有自动恢复库存。
1 | |
Alternative 是一个接受 Stock 与 Payment 参数的普通用例函数,得到同样可区分结果;只有一个调用点、不需稳定对外接口时,它少一个持有三个依赖的对象。Facade 强调为多个对象之间的协作提供更简单的入口;应用服务也可能恰好承担这种编排。名称无法替代失败契约:应明确每个步骤成功后下一步失败会留下什么状态。与 21 的 Adapter(转换不兼容接口)和 27 的 Proxy(控制访问及加载)相比,此处主要问题是让调用者不必知道多步协作顺序,不是改变报价器接口或给它叠加折扣。
| 方案 | 调用方看到的失败 | 扣款失败后库存 |
|---|---|---|
Before |
false,无法分辨缺货与扣款 |
0 |
After |
NO_STOCK 或 PAYMENT_FAILED |
0,明确暴露未恢复 |
Alternative |
同样区分两种错误 | 0,简单函数同样不提供原子性 |
这个项目是单线程内存对象,不涉及数据库事务、远程支付、幂等请求或自动补偿。要提供库存回收,必须另外定义释放条件、状态转换以及释放失败怎么办;绝不能从“有 Facade”推断跨模块操作原子成功。
flowchart LR
Caller[用例调用方] --> Facade[After]
Facade --> Quote[Quote]
Facade --> Stock[Stock]
Facade --> Payment[Payment]
Payment --> Result[Result 状态]
Stock --> Result
实验与练习
运行 ./mvnw -B -ntp -pl labs/25 -am test 和累计 ./mvnw -B -ntp verify,实际环境、命令、输出与退出码见 examples/design-patterns/evidence/25/RUN.md。正例是成功单与可辨认缺货;反例是旧布尔值混淆两类失败;边界是扣款失败的部分状态和 0 分不预留。
- 新增“扣款失败须释放已预留库存”,先加失败及释放失败的断言,再实现释放行为并复跑 900 分旧成功合同;写清楚这不是自动事务回滚。
- 如果只有一个调用者,改用
Alternative函数并保留全部状态断言,说明什么时候保留 Facade 能真正减少调用方知识。
参考资料
- GoF 原书公开图书馆 PDF,4.5 Facade,目录页标注起始页 208:https://cpcc.chd.gov.in/Content/PDFs/w8UkV3tWNyEtsbUZSWJ7fVhuB9A3tsGYdm4w6VGzg2wUTNFYikqnvvFbbkiW2zmfspPEghd7QTamiMby3lVIBemrhdVWwt6rOQnm.pdf 。
- Facade 模式出版社摘录,Pearson/InformIT:https://www.informit.com/articles/article.aspx?p=347700&seqNum=7 。
- 本篇来源边界:
writing-plans/design-patterns/SOURCES.md。 - 实验代码:
examples/design-patterns/labs/25/。
上一节:24 包装行为的先后顺序;下一节:26 哪部分对象值得共享。






