享元模式通过拆分内部状态(不可变、共享)和外部状态(可变、由客户端管理),用工厂集中复用对象,降低内存占用。内部状态如颜色、字号;外部状态如坐标、库存;享元类必须不可变,工厂负责缓存与创建。

直接说重点:享元模式不是“多写几个类”,而是通过拆分状态+集中复用,把成千上万个几乎一样的小对象压成几十个共享实例。关键不在代码结构,而在识别哪些该共享、哪些必须外置。
明确内部状态和外部状态
这是整个模式落地的前提。内部状态是“不变的模板”,外部状态是“每次用时才带上的上下文”。
- 内部状态:所有相同类型对象共有的、不可变的数据,比如商品规格里的“红色”“M码”“棉质”,棋子的“车”“马”“炮”,文本中字符的字形、字体、字号
- 外部状态:每个使用场景独有的、可变的数据,比如某件红色M码商品的ID、当前库存、上架时间;某个“车”的横坐标、纵坐标、是否被选中;某个“a”字符在文档中的行号、列偏移
判断标准很简单:如果两个对象仅靠这部分数据就能完全区分,那它就是外部状态;如果去掉它,对象本质就没了辨识度,那就是内部状态。
用工厂统一管理共享实例
不手动生成对象,也不依赖Spring容器自动注入——享元必须由一个有记忆的工厂来管控。它内部用HashMap或ConcurrentHashMap缓存已创建的享元,键通常是内部状态的组合(如"RED_M_COTTON")。
- 客户端请求一个规格时,传入color="红色"、size="M"、material="棉"
- 工厂拼出唯一key,查缓存;命中则返回已有实例,未命中则new一个并存入
- 工厂本身是单例,且线程安全(尤其在高并发电商系统中)
注意:工厂不保存外部状态,只管“模板有没有”。外部状态永远由调用方持有并传入享元方法中。
享元对象必须不可变
一旦共享,就不能让某个地方改了它,影响其他地方。所以具体享元类里不能有setter方法,所有内部状态只能在构造时赋值,且字段声明为final(Java)或只读属性(Python)。
- 比如ProductSpec享元类里,color、size、material都只读
- 如果后续需要支持动态更新字体样式,那就说明这个属性其实不该放内部状态里——得重新划归外部状态,或者拆出新享元
不可变性不是约束,而是保障。它让共享变得安全、可预测。
客户端要主动承担外部状态管理
享元本身不存位置、不存ID、不存价格。这些信息必须由业务代码自己维护,并在调用享元行为时显式传入。
- 例如渲染一个字符:“请用【字体=微软雅黑、字号=14】这个享元,在(x=120, y=85)处绘制字符‘A’”
- 例如展示商品:“请用【红色-M-棉】这个享元,结合商品ID=PROD-789、库存=23、价格=¥199.00来生成前端卡片”
这意味着客户端代码会多一行参数传递,但换来的是内存占用从O(N)降到O(1)级(N为对象总数,1为共享模板数)。在千万级商品或百万字符文档中,效果立竿见影。










