设计模式 10:只读库存为什么还得实现扣减方法
报价页面只需要回答“库存 3 件,能否预留 2 件”,不负责真正扣减。但早期统一的 InventoryPort 同时列出 available、take 和 restock。只读快照 ReadOnlyBefore 为了给页面提供 available,不得不实现后两个方法,运行时统一抛 UnsupportedOperationException。页面的查询本来能工作;问题是它在类型层面承诺了自己做不到的操作。
先看谁会调用协议
Before.canReserve 只调用一次 inventory.available(sku)。三种设计使用合成库存 paper=3 的共同合同:请求 2 返回 true,请求 4、未知 SKU 和请求 0 返回 false。旧实现没有在此查询上出错;反例发生在同一个 InventoryPort 类型对别的调用方也暴露了 take、restock。实验分别调用这两个占位方法,断言它们抛 UnsupportedOperationException,随后库存仍为 3。Java SE 21 UnsupportedOperationException Javadoc将它描述为所请求操作不受支持;测试命中异常并不是这个只读协议设计“正确”的证明。
After 改让查询客户端依赖 StockLookup.available。只读 ReadOnlySnapshot 只实现该协议,类本身没有 take、restock;测试核对 StockLookup 的公开方法表只含 available,快照也不是 StockMutations。需要写操作的调用方使用 StockMutations:它继承查询,并把有关联的 take 与 restock 两项写行为放在同一协议里。MutableStock 先从 3 扣 2 得 1,再补 1 得 2,随后尝试扣 3 时拒绝且库存仍为 2。没有为凑“最小接口”把两项相关写操作再切成两个单方法接口。
1 | |
flowchart LR
BeforeReader[Before 查询方] --> Large[InventoryPort: available / take / restock]
Large --> OldSnapshot[ReadOnlyBefore: take/restock 占位异常]
AfterReader[After 查询方] --> Lookup[StockLookup: available]
Lookup --> Snapshot[ReadOnlySnapshot]
Mutations[StockMutations: take + restock] --> Lookup
Mutations --> Writable[MutableStock]
| 客户端实际需要 | 旧公共接口 | 拆分后 | 直接替代 |
|---|---|---|---|
| 页面查询库存是否足够 | InventoryPort 暴露三方法 |
StockLookup 只暴露查询 |
Alternative 直接持有复制后的 Map |
| 库存写入方扣减、补回 | 只读实现也必须写两个占位方法 | StockMutations 聚合两项写操作 |
本篇替代方案无写入能力,不冒充写入方 |
Robert C. Martin 在 SOLID Relevance解释 ISP 时,强调别让使用者依赖不需要的接口能力,并指出静态类型语言下无关依赖还可能带来编译和部署传播。这里没有跑构建缓存或部署实验,只验证了本地公开方法面与读写合同;不能从这些断言推断实际重编译次数减少。若页面只在一个地方从内存 Map 查库存,Alternative 的直接查询比额外协议更简单,它也通过原有只读合同。
这不是生产级库存实现。ReadOnlySnapshot 用 Map.copyOf 保存当前合成输入的不可修改副本;MutableStock 在进程内维护 HashMap,没测并发竞争、整数溢出或跨服务补偿。04 篇解释的运行期接口分派在这里仍适用,但它不能把只读实现变成可写实现。06 篇的变化来源提示也一样:分协议是为不同调用方的需求,不是为了让目录看起来层数更多。
验证与练习
2026-10-02 用 Ubuntu OpenJDK 21.0.12.1、Maven Wrapper 3.9.9 执行 ./mvnw -B -ntp verify,退出码 0;10 的 6 次测试、00–09 的 74 次回归全部通过。共同查询入口、负例异常和写入后的状态断言及原始输出在 examples/design-patterns/evidence/10/。
- 写入方新增“按两次操作批量预留”:先约定库存不足时整批拒绝且库存不变,并写可失败测试。将需求加入
StockMutations或写入服务,不要把批量写入塞进StockLookup;比较旧大接口又迫使只读实现添加何种占位方法。尚未测并发,不能把单线程断言当作事务保证。 - 报价页面需要显示读取时刻,而不是只返回库存数。先写读者对“数量与时间属于同一次快照”的断言,判断应扩展
StockLookup结果类型还是另建查询协议;若只把接口拆成更多单方法类型,是否真的减少客户端必须知道的概念?
参考与系列导航
- Robert C. Martin:SOLID Relevance(ISP 段落):接口应符合使用者所需能力。
- Java SE 21:UnsupportedOperationException:不支持操作的异常 API。
- 09 LSP 里氏替换 ← 10 ISP 接口隔离(本篇) → 11 DIP 依赖倒置。





