下单先报价、再预留库存、最后模拟扣款。旧调用者只拿到一个布尔值:缺货是 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
2
3
if (!stock.reserve()) return new Result(NO_STOCK, cents);
if (!payment.charge(cents)) return new Result(PAYMENT_FAILED, cents);
return new Result(OK, cents);

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 分不预留。

  1. 新增“扣款失败须释放已预留库存”,先加失败及释放失败的断言,再实现释放行为并复跑 900 分旧成功合同;写清楚这不是自动事务回滚。
  2. 如果只有一个调用者,改用 Alternative 函数并保留全部状态断言,说明什么时候保留 Facade 能真正减少调用方知识。

参考资料

上一节:24 包装行为的先后顺序;下一节:26 哪部分对象值得共享。