一、享元模式概念
“享元”,顾名思义就是被共享的单元。享元模式的意图是复用对象节省内存,前提是享元对象是不可变对象。
我们将对象的信息分为两个部分: 内部状态和外部状态。
内部状态是对象内部共享的属性,不随环境的改变而改变。
外部状态不可以被共享的,会随环境改变而变化的属性。
享元模式具体来说就是当一个系统中存在大量相似对象的时候,可以将这些对象中相同且不变的部分(不可变的相同属性)提取出来,设计成享元类对象,让这些大量相似的对象引用这些享元对象。
这里所说的内部状态的属性是指通过构造函数初始化后,这些成员变量就不会再被修改了。
下面是享元模式的类图和角色如下(享元的英文就是FlyWeight,FlyWeight的原义是轻量的意思,可见享元模式在节省内存、轻量化对象的作用):
FlyWeight:享元类的接口,operation方法是一些操作外部状态属性的方法,当然外部状态属性需要作为参数传入operation中。
ConcreteFlyWeight:具体的享元类,实现接口FlyWeight,存储内部状态属性。
FlyWeightFactory:享元工厂,用来创建和管理享元对象,其主要思想就是用一个Map来保存已经创建的享元对象实例。它主要是用来确保合理的共享FlyWeight对象,当用户请求一个FlyWeight对象时(getFlyWeight()方法),工厂提供一个已经存在的FlyWeight实例,如果不存在则创建一个返回。享元工厂保证一个享元对象只会有一份从而节省内存。
下面我们举两个例子来看看享元模式如何使用。
象棋游戏中,我们可以抽象出两个对象:棋局和棋子。棋子的属性包括:棋子类型(将、相、士、炮、车、兵等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。而棋局需要保存所有棋子对象。利用这些数据,我们就能显示一个完整的棋盘给玩家。
想想以前我们玩的QQ游戏,一个游戏厅里有成千上万个房间,一个房间有一个棋局,为了记录每个房间当前的棋局情况,我们需要给每个房间都创建一个 ChessBoard 棋局对象和几十个棋子对象,会消耗大量的内存。
此时可以用到享元模式将相似对象中相同且不变的属性提取出来,id、text、color 属性在不同的房间中是不变,唯独 positionX、positionY 不同。
因此可以将棋子的 id、text、color 属性拆分出来,设计成独立的类,并且作为享元供多个棋盘复用,棋盘只需记录每个棋子的位置信息就可以了。
具体的代码实现如下所示:
它的代码实现非常简单,主要是通过工厂模式,用一个 Map 来缓存已经创建过的享元对象,来达到复用的目的。
我们在看第二个例子。
文本编辑器中,要在内存中表示一个文本文件,只需要记录文字和格式两部分信息就可以了,其中,格式又包括文字的字体、大小、颜色等信息。
文本文件中最小的组成单元是文字,一个文字就会包含属于自己的文字内容、字体、大小和颜色。但是按照正常的用户使用习惯,没有人会把一篇文章的所有文字设置成不同的格式。顶多就是标题是一个样式、正文是一个样式、正文中需要强调的内容加粗一下。
此时我们也可以使用享元模式,将相同样式的文字的样式作为享元对象存储起来。具体实现如下:
二、享元对象的优缺点
享元模式是一个非常简单的模式, 它可以大大减少应用程序创建的对象,减低程序内存的占用,增强程序的性能,但它同时也提高了系统复杂性,需要分离出外部状态和内部状态, 而且外部状态具有固化特性,不应该随内部状态改变而改变,否则导致系统的逻辑混乱。
三、享元对象与单例、缓存、对象池的区别
享元模式 VS 单例
在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。
实际上,享元模式有点类似于之前讲到的单例的变体——多例。
从设计意图上来看,享元模式和单例是完全不同的。应用享元模式是为了对象复用,单纯是为了节省内存;而应用单例/多例模式是因为某个实体对象在系统中只应该保存一份,顺便节省了内存。
享元模式 VS 缓存
在享元模式的实现中,我们通过工厂类来“缓存”已经创建好的对象。这里的“缓存”实际上是“存储”的意思,跟我们平时所说的“数据库缓存”“CPU 缓存”“MemCache 缓存”是两回事。
我们平时所讲的缓存,主要是为了提高访问效率,而非复用。
享元模式 VS 对象池
对象池、连接池(比如数据库连接池)、线程池等也是为了复用。
但池化技术中的“复用”可以理解为一种“独占式的重复使用”,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。但在任意时刻,每一个对象、连接、线程,并不会在多个地方同时被使用,而是被一个使用者(线程、协程)独占(此时其他使用者无法占用这个连接),当使用完成之后,放回到池中,再由其他使用者重复利用。
享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。
故池化技术中的对象同一时刻只能被独占,其复用是为了节省初始化时间;而享元模式是同一时刻可被多个地方共享,其复用是为了节省内存。