订单刚建好还是草稿,不允许直接付款。旧对象却只有三个互不约束的布尔字段,pay() 无条件把 paid 置为真。再增加取消、重复确认和重复付款时,靠调用方记住全部组合很容易漏检查:草稿、已确认、已支付、已取消哪一步能做什么?

把迁移合同放到当前阶段

labs/33 的 Before 测试保留“草稿也能 paid=true”的错误反例。After 持有 Phase:草稿允许确认或取消;已确认允许支付或取消,重复确认返回原阶段;已支付允许重复支付但不再次做事,取消则拒绝;已取消不再付款。非法操作抛异常,失败后当前阶段不变。测试旧正常路径“确认 → 付款”仍达已支付,新增草稿付款拒绝、取消后付款拒绝、重复调用幂等均有断言。

1
2
3
public void pay() {
phase = phase.pay();
}

此处 State 的变化来自同一个订单自身的生命周期:不同阶段决定允许哪些行为。与 28 的 Strategy 不同,后者由外部选定报价算法,本章阶段按操作迁移;与 29 的 Template Method 不同,这里并无固定的“读/写”骨架等待子类覆写。Alternative 用一个字符串状态和显式条件完成同样的小型状态机;四个阶段且规则简单时,比四个阶段类型更省代码。不要为任何两个 if 都建立 State 层次。

方案 草稿付款 重复付款、取消
Before 错误地置为已支付 多个布尔值可相互矛盾
After 拒绝且保持草稿 本例重复付款不做事;已支付不可取消
Alternative 同样拒绝 用显式分支维护教学合同

重复支付的幂等只是本地阶段对象的返回值;没有真实收款请求、外部侧效、跨线程读写或数据库状态迁移。一旦接入支付端口,必须另行约定请求键、外部结果与重试,不能把 Paid.pay() { return this; } 当作真实支付幂等保证。

flowchart LR
    Caller[订单操作方] --> Order[After 持有 Phase]
    Order --> Phase[Phase 操作接口]
    Phase --> Draft[Draft]
    Draft --> Confirmed[Confirmed]
    Confirmed --> Paid[Paid]
    Draft --> Canceled[Canceled]
    Confirmed --> Canceled

验证与练习

运行 ./mvnw -B -ntp -pl labs/33 -am test 与累计 ./mvnw -B -ntp verify,各步输入、异常、环境和原始输出见 examples/design-patterns/evidence/33/RUN.md。

  1. 新增“已确认但未支付可以回到草稿”,先写所有旧迁移合同与新回退断言,再对两个实现分别补迁移,检查取消后不会被回退复活。
  2. 如果产品删除取消流程,只保留草稿、已确认、已支付三个状态,比较压缩成枚举 switch 与保留阶段类型的代码量和测试可读性。

参考资料

上一节:32 审核何时停止传递;下一节:34 遍历协议隐藏了什么。