两个订单请求都买 paper,第一个数量 1,第二个数量 9。为了少分配对象,把整条“商品 + 数量”放进同一个缓存会怎样?第一个调用者握着的对象随后读到数量 9。共享身份本身不是问题,问题是把每次请求才有的可变数量一起共享了。

把内在数据和请求数据分开

Before.line("paper", 1) 返回缓存中的可写对象;第二次请求同一 SKU 数量 9,反例断言两次拿到同一引用,第一个对象的 quantity 也变成 9。After 只在本地 Map 中共享只读 Descriptor(sku, description);每次调用新建只读 Line(descriptor, quantity)。测试断言描述对象引用相同,两个行对象不是同一引用,第一个数量仍为 1、第二个是 9,本地描述缓存总数为 1。非法数量在插入缓存之前拒绝,失败不会意外新增缓存条目。

1
2
3
Descriptor descriptor = shared.computeIfAbsent(
sku, key -> new Descriptor(key, "name:" + key));
return new Line(descriptor, quantity);

共享与请求分离是本章 Flyweight 的意图:描述是与具体订单无关的内在状态,数量是请求上下文提供的外在状态。Alternative 对每个请求直接创建一个值对象,结果语义相同却不复用引用;如果商品描述少、对象成本没有被测出问题,它避免缓存失效、内存占用和同步方面的额外责任。一个普通缓存不因使用 computeIfAbsent 就自动成为适合共享任意状态的享元。示例 Map 不是并发缓存,也没有替换商品名称后使已有对象失效的协议。

方案 同 SKU 描述与数量 第二次请求能否改变第一次结果
Before 整条可写记录共享 能,数量从 1 变 9
After 描述共享、数量分别存放 不能;只读行保留 1
Alternative 都分别创建 不能;没有缓存维护成本
flowchart LR
    Caller[行项目创建方] --> Pool[After 描述池]
    Pool --> Descriptor[Descriptor 共享只读描述]
    First[Line 数量 1] --> Descriptor
    Second[Line 数量 9] --> Descriptor

验证与练习

在 examples/design-patterns/ 运行 ./mvnw -B -ntp -pl labs/26 -am test、./mvnw -B -ntp verify;原始命令、环境和结果见 examples/design-patterns/evidence/26/RUN.md。正例为描述可共享,反例为数量共享污染;边界为数量 0 以及失败时缓存数不变。未测堆内存、吞吐量、并发或数据库数据新鲜度。

  1. 新增“商品描述会改名”,先断言旧的订单行保留还是更新(必须选一个合同),再增加失效或版本规则并复跑 1/9 数量隔离断言。
  2. 删除缓存改用 Alternative;在不能提供实际内存收益证据时说明为什么这是合适的简化,而不是机械地保留享元。

参考资料

上一节:25 用例入口如何暴露失败;下一节:27 谁能读取延迟加载的库存。