什么是享元模式?它解决什么问题?
简化版
享元模式是一种通过共享细粒度对象来减少内存占用的结构型设计模式。它适合系统中存在大量相似对象,并且这些对象可以拆分出可共享的内部状态和不可共享的外部状态的场景。
详细版
享元模式的核心是“能共享的只创建一份,不能共享的由外部传入”。比如围棋棋子、文本编辑器里的字符对象、游戏里的树木模型,都可能有大量重复对象。
享元模式通常会把对象状态拆成两类:
- 内部状态:对象自身固定、可共享的部分,例如棋子颜色、字符编码、树木类型。
- 外部状态:随使用场景变化、不可共享的部分,例如棋子坐标、字符位置、树木坐标。
享元对象只保存内部状态,外部状态在调用方法时传入。这样大量对象可以复用少量共享实例,减少内存和创建成本。
完整版教学
一、为什么需要享元模式
有些系统会创建非常多的小对象。单个对象看起来不大,但数量一上来,内存压力就很明显。
例如一个地图游戏中有 10 万棵树。如果每棵树都保存完整图片、颜色、树种、纹理、坐标、生命值等信息,内存会被大量重复字段占用。
但仔细看会发现:
- 树种、颜色、纹理这类信息,很多树是一样的。
- 坐标、当前状态这类信息,每棵树不同。
享元模式就是把相同部分抽出来共享,把不同部分留在外部管理。
二、享元模式的定义怎么理解
享元模式通过共享对象来支持大量细粒度对象。这里的“享元”可以理解为“被共享的单元”。
它不是简单地少 new 几次对象,而是有意识地把对象拆成:
- 可以安全共享的内部状态。
- 由调用方提供的外部状态。
享元对象本身通常是不可变的,因为不可变对象更容易被多个地方安全共享。
三、典型例子:围棋棋子
围棋棋子数量很多,但棋子类型只有黑棋和白棋。
可以共享的内部状态:
- 颜色:黑色或白色。
- 显示样式:棋子图片、渲染规则。
不可共享的外部状态:
- 棋子所在坐标。
- 当前棋盘引用。
系统只需要创建一个黑棋对象和一个白棋对象。落子时传入坐标即可。
四、享元模式不是普通缓存
享元模式常常通过缓存实现,但它不等于缓存。
缓存关注的是避免重复计算或重复加载,缓存对象可能本身带有完整状态。享元模式关注的是对象状态拆分,通过共享内部状态降低大量对象的内存占用。
如果只是把查询结果放进 Map,下次直接取出来,那通常是缓存;如果为了大量对象共享内部状态、外部状态由调用方传入,那更接近享元。
五、什么时候收益明显
享元模式需要满足几个条件才有明显价值:
- 对象数量非常多。
- 对象之间有大量重复状态。
- 重复状态可以安全共享。
- 外部状态可以从对象内部剥离。
- 对象创建或内存占用是实际问题。
如果对象数量很少,或者对象状态几乎都不同,享元模式就不值得引入。
六、常见误区与追问
享元模式通过共享重复的内部状态,减少大量细粒度对象的内存。若10000个字符只使用20种字形配置,就保留20个享元,字符位置作为外部状态传入,而不是保存10000份字体数据。它优化的是状态表示,不只是把构造结果塞进 Map。
| 检查维度 | 判定依据 |
|---|---|
| 内部状态 | 存进享元并被多个上下文共享 |
| 外部状态 | 由上下文保存,调用时传入 |
N 个上下文 -> K 个享元,且 K << N
记忆钩子:把重复的“重部分”抽出来共享,把变化的“轻部分”留在现场。
- 误区:享元模式要求系统只有一个对象。 它通常按内部状态键维护多个共享实例。
- 追问:为什么属于结构型模式? 它通过上下文与共享对象的组合重构对象表示。
- 误区:共享对象可以保存调用位置。 位置属于外部状态,写入共享对象会污染其他上下文。
- 追问:它与对象池的根本区别是什么? 享元可同时被多方共享,对象池资源通常一次借给一个使用者。
- 追问:何时收益最大? 对象数量大、内部状态体积大且种类远少于对象数时。
七、加强记忆
享元模式的记忆点是“大量小对象,拆状态再共享”:把对象里稳定、重复、可共享的部分放进享元对象,把变化的部分放到外部传入,用少量共享对象支撑大量使用场景。