1. 概述
享元模式是一种结构型设计模式, 它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。
从问题引入,假如你希望在长时间工作后放松一下, 所以开发了一款简单的游戏: 玩家们在地图上移动并相互射击。 你决定实现一个真实的粒子系统, 并将其作为游戏的特色。 大量的子弹、 导弹和爆炸弹片会在整个地图上穿行, 为玩家提供紧张刺激的游戏体验。
尽管该游戏在你的电脑上完美运行, 但是你的朋友却无法长时间进行游戏: 游戏总是会在他的电脑上运行几分钟后崩溃。 在研究了几个小时的调试消息记录后, 你发现导致游戏崩溃的原因是内存容量不足。 朋友的设备性能远比不上你的电脑, 因此游戏运行在他的电脑上时很快就会出现问题。
真正的问题与粒子系统有关。 每个粒子 (一颗子弹、 一枚导弹或一块弹片) 都由包含完整数据的独立对象来表示。 当玩家在游戏中鏖战进入高潮后的某一时刻, 游戏将无法在剩余内存中载入新建粒子, 于是程序就崩溃了。
那该怎么办呢?该怎么办也让朋友的拉跨电脑也能玩这个游戏呢?既然内存不够,那我就看能不能减少点内存使用。很快哈,你就会发现原来每一个子弹的对象很多东西其实都是一样的,除了可能位置、颜色等属性不一样,其他内容都是一样的,所以你可能就有了一个想法:我能不能复用这些属性,每次使用的时候只修改一些特有属性,从而达到节省内存花销。
如果你有这个想法,恭喜你你有了享元模式的思想。是的享元模式一大特点就是共享了对象的部分内容,把一个对象分为了两部分,同时这个被分成两部分的对象也叫做享元对象。
- 外部状态:对象可变的状态,不复用的部分,像上面问题中的子弹的颜色、位置
- 内部状态:对象不变或者十分稳定的状态,可复用的部分
那又该怎么实现享元模式呢,或者说该怎么实现对象的复用呢。其实说到底,享元模式的一个核心就是使用了复用技术,其实这种使用在我们日常是很常见的,比如JDBC的连接池、多线程的线程池,都是复用的技术。那我们可不可以借鉴一下,也是用一个池,用来存放以及共享的对象。
事实上,享元模式的确就是这么做的,享元模式和缓存池的实现很像,但享元模式里不叫池叫做享元工厂,这个工厂缓存了内部状态,让它得以被复用,所以知道池技术的小伙伴享元模式应该很好理解。具体看下面的代码实现。
- 总结
- 享元模式的核心就是实现了对象的复用,在某种角度上来说就是实现了一个单例的内部状态(享元模式不是单例模式)
- 享元模式的实现和我们日常所见的池技术很像,建一个类存放共享对象,每次需要的时候就向这个类申请
- 享元对象把一个类分成两部分,一部分为不稳定的状态,叫外部状态,不可共享;一部分很稳定,叫内部状态,可共享
- 享元模式仅在程序必须支持大量对象且没有足够的内存容量时,否则会造成逻辑的混乱
2. 特点
-
优点: 如果程序中有很多相似对象, 那么你将可以节省大量内存。
-
缺点
- 你可能需要牺牲执行速度来换取内存, 因为他人每次调用享元方法时都需要重新计算部分情景数据。
- 代码会变得更加复杂。 团队中的新成员总是会问: “为什么要像这样拆分一个实体的状态?”
-
使用场景:仅在程序必须支持大量对象且没有足够的内存容量时使用享元模式。应用该模式所获的收益大小取决于使用它的方式和情景。 它在下列情况中最有效:
- 程序需要生成数量巨大的相似对象
- 这将耗尽目标设备的所有内存
- 对象中包含可抽取且能在多个对象间共享的重复状态。
3. 实现
下面举一个例子来实现,植树节来了,现在要植树小程序,我们都知道,大多数树长得其实都差不多,所以如果每次都单独建一个树的对象,其实很浪费内存,所以使用享元模式来实现。
把树看成享元对象,分成内部状态TreeInside
,这个包含了树的种类;外部状态TreeOutside
,这部分包含了树的高度和宽度。具体实现看下文:
-
UML类图
-
角色
-
内部状态:享元对象的共享部分,内部状态要依赖外部状态,有时看需求也可以改成组合,反正就是要建立起与外部状态的联系,组成一个完整的享元对象。
-
外部状态:享元对象的不可共享部分
-
享元工厂:不用把享元工厂看的那么复杂,其实他就是个生产内部状态的缓存池,所以它与内部状态有关系,和外部状态没关系
-
客户端:调用享元模式的角色
-
-
Java实现
-
内部状态
/** * @Author: chy * @Description: 享元对象的内部状态,树的种类 * @Date: Create in 14:23 2021/3/15 */ public class TreeInside { private String name; public TreeInside(String name) { this.name = name; } public void plant(TreeOutside treeOutside){ System.out.println("种一棵高为"+treeOutside.getTall()+"米宽为"+treeOutside.getWidth()+"米的"+name); } }
-
外部状态
/** * @Author: chy * @Description: 享元对象的外部状态,树的高度和宽度 * @Date: Create in 14:24 2021/3/15 */ public class TreeOutside { private int tall; private int width; public TreeOutside(int tall, int width) { this.tall = tall; this.width = width; } public int getTall() { return tall; } public int getWidth() { return width; } }
-
享元工厂
/** * @Author: chy * @Description: 享元工厂,存放享元对象的内部状态:树的生产工厂 * @Date: Create in 14:27 2021/3/15 */ public class TreeFactory { private static HashMap<String,TreeInside> pool = new HashMap<>(); public static TreeInside getTree(String name){ if (!pool.containsKey(name)){ pool.put(name,new TreeInside(name)); } return pool.get(name); } }
-
客户端
/** * @Author: chy * @Description: 客户端 * @Date: Create in 15:11 2021/3/13 */ public class Client { public static void main(String[] args) { // 种十棵橡树 和 十 for (int i = 0; i < 10; i++) { TreeInside treeInside1 = TreeFactory.getTree("橡树"); treeInside1.plant(new TreeOutside(i+1,i+1)); } System.out.println("========================================"); // 种十棵榕树 for (int i = 0; i < 10; i++) { TreeInside treeInside2 = TreeFactory.getTree("榕树"); treeInside2.plant(new TreeOutside(i+1,i+1)); } } }
-
结果
-
实际上,享元模式远没有上面这么简单,享元模式的实现算是一个比较复杂的东西,但上面的大家意会就好,总结一下,就几句话:享元模式把一个类分成两个部分,一部分可共享一部分不可共享,两部分通过依赖或者组合的关系关联起来,可共享的部分建一个类似于缓存池的类存起来,这样就实现类复用