享元模式
一、概述
一般问题:很多情况下需要在系统中增加大量相似对象的个数,从而将导致运行代价过高,性能下降。
核心方案:运用共享技术支持大量细粒度对象的复用,从而节约内存空间,提高系统性能。
设计意图:首先,享元模式要求能够共享的对象必须是细粒度对象——相似度高、状态变化小。既然相似度高,从面向接口编程的思想出发,我们自然会想到先定义一个抽象享元类flyweight;其次,享元模式的核心是复用已经存在的对象,减少了new的次数,那自然需要有个容器来管理这些对象,因此需要定义一个享元工厂类flyweightfactory。
享元模式的一般设计图如下:
flyweightfactory本质上是一个对象池,现实中可以是自定义的工厂类,也可以是list、stack等系统容器类。
共享维度:享元模式的核心是复用,而复用的对象又有粒度之分:
- 一种是复用整个体享元对象,比如线程池、java字符串等,最终减少了对象创建和销毁的次数。
- 另一种是将享元对象拆分为不变的内部状态和可变的外部状态,从而最大限度的复用其内部状态,极大地减少了享元对象的个数。
二、复用享元对象
复用享元对象不难理解,我们以密码输入框中的圆点为例:
以charstate类来表示圆点,用户每输入一个字符都会新建一个charstate实例;同样,用户每删除一个字符都会释放一个charstate实例。而实际操作中,会出现反复的输入和删除字符,这样频繁的创建和销毁实例必然增加系统开销。
我们用享元模式来重新设计:
我们为charstate设计一个栈容器stack<charstate>:
- 当输入字符时:检查栈是否为空,如果为空,则新建一个charstate实例;否则直接从栈中pop一个charstate实例
- 当删除字符时:直接将对应的charstate实例push入栈
这样,无论用户如何操作,charstate实例的个数都不会超过密码的最长位数。
对应代码如下:
private charstate obtaincharstate(char c) { charstate charstate; //如果pool为空则新建charstate实例,否则直接pop一个实例 if(mcharpool.isempty()) { charstate = new charstate(); } else { charstate = mcharpool.pop(); charstate.reset(); } charstate.whichchar = c; return charstate; }
三、复用享元内部状态
相比复用享元对象,复用内部状态更复杂一点。在享元模式中,可以共享的相同内容称为内部状态(intrinsic state),而那些需要外部环境来设置的不能共享的内容称为外部状态(extrinsic state),由于区分了内部状态和外部状态,因此可以通过设置不同的外部状态使得相同的对象可以具有一些不同的特征,而相同的内部状态是可以共享的。如下图:
这里的重点是区分哪些是内部状态,哪些是外部状态。依然以密码输入框的圆点为例:
- 所有圆点的形状、大小、颜色都是一样的,这些都属于内部状态;
- 唯一不同的是各个圆点的位置,这是外部状态。
也就是说,我们只需要一个charstate实例和一个存储位置的外部列表就行了。
重新设计后的类图如下:
- charstatefactory负责缓存charstate实例,实际上只需要创建一个charstate实例
- client维护了一个位置列表,代表各个圆点的位置
- charstate定义了draw(int translationx)方法,在需要绘制的时候,由client传入位置信息
charstatefactory代码简化为:
private charstate charstate; public charstate obtaincharstate(char c) { //如果charstete为null 则新建charstate实例 if(charstate == null) { charstate = new charstate(); } return charstate; }
至此,由最开始每次输入都要new一个charstate对象,到最多new密码限制位数的charstate对象,再到只需要new一个charstate对象。可见,享元模式在消灭对象个数,节约内存方面的确效果显著!
四、总结
优点:享元模式的优点在于它可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份。
缺点:享元模式使得系统更加复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
总结:享元模式是结构型设计模式,是一个考虑系统性能的设计模式,通过使用享元模式可以提高对象复用,节约内存空间,提高系统性能。