设计模式 12:谁该决定库存预留
订单界面要预留两件 paper。老调用者先找到订单里的预留记录,再找到货架,最后取出库存 Map;它自己比较剩余数,并写回扣减结果。今天的合同是:初始库存 3,预留 2 后剩 1;数量为零或库存不足要拒绝,并且拒绝后数量不变。明天的需求是“单次最多预留 2 件”。这条规则究竟该由谁保证?
知道货架结构的调用者
旧版 Before.reserve 中的关键两步来自可执行实验:
1 | |
实际代码还在写回前检查了非正数量和库存不足。问题不是有四个点号,而是调用者知道 Order → Reservation → Shelf → Map 的路径,且任何拿到 order() 的代码都能越过检查直接修改库存。实验直接执行 client.order().reservation().shelf().stock().put("paper", -5);后续查询读到 -5。新增上限规则时,旧调用者允许预留 3 件,余额从 4 变成 1;这是受测的旧行为,不是本章新增合同通过。
把预留命令交给维护状态的一方
After 的 Order 保存私有库存副本,预留入口集中检查 1 <= quantity <= 2、剩余数及扣减。外部只发出 reserve(sku, quantity);调用者拿到的是 Availability(sku, remaining) 值投影,不是可写 Map 或货架。构造后的调用方修改原始输入 Map,也不会把余额变成 99。
1 | |
这段逻辑在 After.Order.reserve 内;其余业务端不必知道货架的嵌套路径。另一种 Alternative 不建订单内层对象,直接在单个库存类上完成同样的校验、扣减与查询。如果目前只有一个小范围调用者、也没有复杂的订单协作,单类实现更便宜;不必为消灭每一个点号再造一串 getShelfStockRemaining() 转发方法。
flowchart LR
Old[Before 调用者] --> Record[Reservation]
Record --> Shelf[货架]
Shelf --> Mutable[可写库存 Map]
Caller[After 调用者] --> Command[Order.reserve]
Command --> Private[私有库存]
Caller --> View[Availability 值投影]
Simple[Alternative 单类] --> Own[本地库存与校验]
| 设计 | 改动“单次最多 2 件”的位置 | 内部结构与查询的边界 |
|---|---|---|
Before |
依赖嵌套路径的调用者必须逐一补检查;本实验旧版仍允许 3 件 | order() 暴露可写 Map,可从外面直接写入 -5 |
After |
Order.reserve 内检查,拒绝时不改变状态 |
availability(sku) 返回独立的 Availability 值 |
Alternative |
单类 reserve 内检查,拒绝时不改变状态 |
同样返回值投影;若职责始终很小,不必引入内层订单对象 |
这里的修改范围是本地代码与测试对比,不是已经证明任何真实项目能自动完成重构。After 仍有一层对订单的用例入口转发,这是一个明确的边界;若没有这个边界的使用者,就选更直接的 Alternative。没有做并发预留或持久化测试,不能把单线程 Map 的检查与写入声称为数据库库存的原子操作。
Northeastern Demeter 团队的 Law of Demeter 通用表述强调对其他单元的知识应有限,并区分类形式与对象形式;对象形式列出方法可以与参数、自身、直接组成部分及方法中创建的对象交互。这不是“所有包含两个点号的语句都违法”的语法检查。Martin Fowler 在 Tell, Don’t Ask 中也指出,行为与数据就近放置有用,却不该因此清除所有查询;对外提供恰当的信息变换或只读投影可能更简单。因此本例把预留决策放回维护状态的对象,仍保留 availability 给展示方读数;避免把视图决定放进库存模型。
验证与练习
在 Ubuntu OpenJDK 21.0.12.1、Maven Wrapper 3.9.9、JUnit Jupiter 5.11.4 下执行 ./mvnw -B -ntp verify,退出码 0。本篇 7 项测试(共同合同 3、反例与新增需求 4)和 00–11 的 93 项回归均通过。完整命令、环境及压缩原始输出在 examples/design-patterns/evidence/12/;此处没有把 Before 的旧上限行为说成新增合同通过。
- 增加“每次预留后返回剩余数量”的需求。先让三个变体执行相同的成功、库存不足和零数量断言;再设计返回值,检查失败时是否误报旧库存或意外扣减。说明何时
Availability值投影足够,何时必须由对象执行命令。 - 假设要为展示方显示商品分区,却不允许展示方扣库存。尝试在
Before的路径上增加分区层,记录哪些调用者必须修改;为After与Alternative设计一个稳定的只读投影和失败测试,比较多一个查询与一连串转发方法的代价。
参考与系列导航
- Northeastern Demeter 团队:Law of Demeter 通用表述:关系有限与类/对象形式的区分。
- Northeastern Demeter 团队:对象形式:可交互对象的具体范围。
- Martin Fowler:Tell, Don’t Ask:就近放置行为及查询的适当例外。
- 11 DIP 依赖倒置 ← 12 最少知识(本篇) → 13 DRY、KISS 与 YAGNI(待写)。





