首页/开发/代码质量与评审速查/享元模式 Flyweight(结构型)
设计原则与模式62 条

享元模式 Flyweight(结构型)

共享大量细粒度对象的不可变部分(内部状态),把可变上下文(外部状态)留在调用方:降低内存与构造开销;经典场景是字符排版、地图瓦片、缓存池。

共享不变的部分

享元模式把对象状态拆成两份:可共享且不变的内部状态集中成少量实例,随调用变化的外部状态由使用方在使用时传入。这样数万个「对象」在内存里只对应几十个共享实例,构造与内存开销显著下降。它的前提是对象数量大,且大部分状态确实可以复用。

基本信息

意图共享内部状态、分离外部状态
前提对象数量大且大部分状态可复用
关键被共享的实例必须不可变
配套工厂管理共享池,加弱引用或容量上限控制

典型使用场景

  • 文本编辑器:字符的字体与字号属性共享,位置与颜色逐次传入
  • 地图与游戏:瓦片、植被、粒子按类型共享几何与贴图,坐标作为外部状态
  • 大规模渲染:同一树种共享模型,坐标与缩放外部传入
  • 对象池:数据库连接、线程等昂贵对象的复用
  • 字符串驻留:运行时把相同字面量指向同一实例,思路与此一致

形态示意

形态示意
# 内部状态:可共享,创建后不再变
class TreeType:
    def __init__(self, name, texture):   # 种类与贴图只建一份
        self.name = name
        self.texture = texture

    def draw(self, canvas, x, y):        # 外部状态每次传入
        canvas.paint(self.texture, x, y)

# 森林里几万棵树共享少量 TreeType 实例,只有坐标各自持有
forest = [Tree(tree_types['oak'], x, y) for x, y in coords]

内部状态与外部状态的差别

维度内部状态外部状态
是否可变不可变随调用变化
存放位置共享实例,由工厂持有调用方或上下文对象
典型例子字体、贴图、瓦片数据坐标、颜色、时间戳
误判后果共享可变数据互相串改为共享而复制大量状态,收益归零
共享池要设上限或使用弱引用:只增不减的池最终会变成内存泄漏;用弱引用可以让不再被使用的共享实例被回收,代价是同一类型可能被重建多次。
先度量再动手:用堆直方图、对象计数或内存分析确认对象数量与占用确实是瓶颈,再引入享元。否则多出来的间接层只会增加复杂度,收益无法验证。

易错点

  • 把可变状态也共享:一处修改影响所有使用者,表现为离奇的跨模块脏数据
  • 无度量先上享元:复杂度先行而收益未知,代码变得难读且没有指标支撑
  • 共享实例被就地修改:不可变只是口头约定,缺少语言级保障
  • 外部状态反复重建:每次渲染都新建上下文对象,抵消了共享省下的开销

数据来源与参考

  • 《设计模式》(GoF)· Flyweight
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌