设计模式 33:对象状态如何约束操作
订单刚建好还是草稿,不允许直接付款。旧对象却只有三个互不约束的布尔字段,pay() 无条件把 paid 置为真。再增加取消、重复确认和重复付款时,靠调用方记住全部组合很容易漏检查:草稿、已确认、已支付、已取消哪一步能做什么?
把迁移合同放到当前阶段
labs/33 的 Before 测试保留“草稿也能 paid=true”的错误反例。After 持有 Phase:草稿允许确认或取消;已确认允许支付或取消,重复确认返回原阶段;已支付允许重复支付但不再次做事,取消则拒绝;已取消不再付款。非法操作抛异常,失败后当前阶段不变。测试旧正常路径“确认 → 付款”仍达已支付,新增草稿付款拒绝、取消后付款拒绝、重复调用幂等均有断言。
1 | |
此处 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。
- 新增“已确认但未支付可以回到草稿”,先写所有旧迁移合同与新回退断言,再对两个实现分别补迁移,检查取消后不会被回退复活。
- 如果产品删除取消流程,只保留草稿、已确认、已支付三个状态,比较压缩成枚举
switch与保留阶段类型的代码量和测试可读性。
参考资料
- GoF 原书公开图书馆 PDF,5.8 State,目录页标注起始页 338:https://cpcc.chd.gov.in/Content/PDFs/w8UkV3tWNyEtsbUZSWJ7fVhuB9A3tsGYdm4w6VGzg2wUTNFYikqnvvFbbkiW2zmfspPEghd7QTamiMby3lVIBemrhdVWwt6rOQnm.pdf 。
- State 模式简定义,InfoWorld/JavaWorld:https://www.infoworld.com/article/2170730/design-patterns-the-big-picture-part-1-design-pattern-history-and-classification.html 。
- 本篇来源边界:
writing-plans/design-patterns/SOURCES.md。 - 实验代码:
examples/design-patterns/labs/33/。
上一节:32 审核何时停止传递;下一节:34 遍历协议隐藏了什么。






