设计模式 26:哪部分对象值得共享
两个订单请求都买 paper,第一个数量 1,第二个数量 9。为了少分配对象,把整条“商品 + 数量”放进同一个缓存会怎样?第一个调用者握着的对象随后读到数量 9。共享身份本身不是问题,问题是把每次请求才有的可变数量一起共享了。
把内在数据和请求数据分开
Before.line("paper", 1) 返回缓存中的可写对象;第二次请求同一 SKU 数量 9,反例断言两次拿到同一引用,第一个对象的 quantity 也变成 9。After 只在本地 Map 中共享只读 Descriptor(sku, description);每次调用新建只读 Line(descriptor, quantity)。测试断言描述对象引用相同,两个行对象不是同一引用,第一个数量仍为 1、第二个是 9,本地描述缓存总数为 1。非法数量在插入缓存之前拒绝,失败不会意外新增缓存条目。
1 | |
共享与请求分离是本章 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/9 数量隔离断言。
- 删除缓存改用
Alternative;在不能提供实际内存收益证据时说明为什么这是合适的简化,而不是机械地保留享元。
参考资料
- GoF 原书公开图书馆 PDF,4.6 Flyweight,目录页标注起始页 218:https://cpcc.chd.gov.in/Content/PDFs/w8UkV3tWNyEtsbUZSWJ7fVhuB9A3tsGYdm4w6VGzg2wUTNFYikqnvvFbbkiW2zmfspPEghd7QTamiMby3lVIBemrhdVWwt6rOQnm.pdf 。
- Flyweight 模式简定义,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/26/。
上一节:25 用例入口如何暴露失败;下一节:27 谁能读取延迟加载的库存。






