设计模式 30:请求对象能撤销什么
库存预留可以作为任务排队执行;操作员误触后希望恢复最后一次预留。将 stock::reserve 放进 Runnable 队列已经能排队,却没有“哪条操作成功执行过、能否撤销一次”的信息。撤销的前提、顺序和范围由谁记录?
执行成功后才加入历史
Before.enqueueAndRun 运行一次预留,初始 1 件变 0 件,这一旧合同保持不变。After 命令封装 Stock 与本次是否执行成功的状态:execute() 只允许一次成功预留,库存不足时抛错且不记完成;undo() 仅在成功执行后允许一次释放。Queue.run 在 execute() 完成后将命令压入栈,undoLast() 按后进先出撤销。测试初始 2 件、连续预留两次、按逆序撤销后恢复 2 件;未执行前撤销、重复撤销、空队列撤销都抛错。
1 | |
Alternative 直接暴露 reserve(stock) 和 release(stock):若只在一个同步调用链里操作,没有排队、序列化或历史需求,这更省代码;但调用者要自己保证释放的是哪一次成功预留。Command 的意图是把请求作为对象传递、记录或排队,同时明确接收者与执行边界;接口里有 undo 不代表任何副作用都能逆转。这里没有事务日志、外部支付和跨进程恢复:本地“撤销”是按约定执行一个释放动作,不是数据库事务回滚;“补偿”通常处理已对外生效的动作,有新的失败与重试问题;“重新执行”则可能重复扣库存,不能混称撤销。
| 方案 | 预留 1 件 | 失败或撤销 |
|---|---|---|
Before |
Runnable 可执行 |
无执行历史和撤销 |
After |
命令成功后入栈 | 库存不足不入栈;只能按栈撤销成功命令 |
Alternative |
直接方法 | 调用者自己管理释放前提 |
队列在本例是内存 Deque,不是消息服务;并发和持久化均未测试。Queue.undoLast() 若释放本身失败,现实现会先弹出历史,不能称它已具备重试安全性。
flowchart LR
Caller[命令调用方] --> Queue[Queue]
Queue --> Command[Command execute / undo]
Reserve[After 预留命令] -.实现.-> Command
Reserve --> Stock[Stock]
Queue -.逆序撤销.-> Command
验证与练习
执行 ./mvnw -B -ntp -pl labs/30 -am test 与累计 ./mvnw -B -ntp verify,原始命令、输入、输出和退出码见 examples/design-patterns/evidence/30/RUN.md。失败断言比打印最终库存更重要。
- 加入可失败的
release,先写“释放失败不能无声丢失历史”的断言,再重新设计出栈顺序并复跑原先后进先出与库存不足合同。 - 如果需求只允许同步预留不允许撤销,删去命令对象与历史,改用
Alternative,说明保留哪些行为断言。
参考资料
- GoF 原书公开图书馆 PDF,5.2 Command,目录页标注起始页 263:https://cpcc.chd.gov.in/Content/PDFs/w8UkV3tWNyEtsbUZSWJ7fVhuB9A3tsGYdm4w6VGzg2wUTNFYikqnvvFbbkiW2zmfspPEghd7QTamiMby3lVIBemrhdVWwt6rOQnm.pdf 。
- Vlissides/Schmidt 公开课件
An Introduction to Design Patterns:https://www.dre.vanderbilt.edu/~schmidt/PDF/GoF.pdf 。 - 本篇来源边界:
writing-plans/design-patterns/SOURCES.md。 - 实验代码:
examples/design-patterns/labs/30/。
上一节:29 哪些报告步骤必须固定;下一节:31 谁管理订阅和失败。






