实现对象的复用-享元模式
在软件系统中,有时候也会存在资源浪费的情况,例如在计算 机内存中存储了多个完全相同或者非常相似的对象,如果这些对象的数量太多将导致系统运 行代价过高,内存属于计算机的“稀缺资源”,不应该用来“随便浪费”,那么是否存在一种技术 可以用于节约内存使用空间,实现对这些相同或者相似对象的共享访问呢?答案是肯定,这 种技术就是我们本章将要学习的享元模式。
举个栗子:
围棋棋子的设计
软件公司欲开发一个围棋软件,其界面效果如图:
软件公司开发人员通过对围棋软件进行分析,发现在围棋棋盘中包含大量的黑子和白 子,它们的形状、大小都一模一样,只是出现的位置不同而已。如果将每一个棋子都作为一 个独立的对象存储在内存中,将导致该围棋软件在运行时所需内存空间较大,如何降低运行 代价、提高系统性能是Sunny公司开发人员需要解决的一个问题。为了解决这个问题,Sunny 公司开发人员决定使用享元模式来设计该围棋软件的棋子对象,那么享元模式是如何实现节 约内存进而提高系统性能的呢?别着急,下面让我们正式进入享元模式的学习。
享元模式概述
当一个软件系统在运行时产生的对象数量太多,将导致运行代价过高,带来系统性能下降等 问题。例如在一个文本字符串中存在很多重复的字符,如果每一个字符都用一个单独的对象 来表示,将会占用较多的内存空间,那么我们如何去避免系统中出现大量相同或相似的对 象,同时又不影响客户端程序通过面向对象的方式对这些对象进行操作?享元模式正为解决 这一类问题而诞生。享元模式通过共享技术实现相同或相似对象的重用,在逻辑上每一个出 现的字符都有一个对象与之对应,然而在物理上它们却共享同一个享元对象,这个对象可以 出现在一个字符串的不同地方,相同的字符对象都指向同一个实例,在享元模式中,存储这 些共享实例对象的地方称为享元池(Flyweight Pool)。我们可以针对每一个不同的字符创建一个 享元对象,将其放在享元池中,需要时再从享元池取出。
享元模式以共享的方式高效地支持大量细粒度对象的重用,享元对象能做到共享的关键是区 分了内部状态(Intrinsic State)和外部状态(Extrinsic State)。
下面将对享元的内部状态和外部状态 进行简单的介绍:
(1) 内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,内部状态可以共享。 如字符的内容,不会随外部环境的变化而变化,无论在任何环境下字符“a”始终是“a”,都不会 变成“b”。
(2) 外部状态是随环境改变而改变的、不可以共享的状态。享元对象的外部状态通常由客户端 保存,并在享元对象被创建之后,需要使用的时候再传入到享元对象内部。一个外部状态与 另一个外部状态之间是相互独立的。如字符的颜色,可以在不同的地方有不同的颜色,例如 有的“a”是红色的,有的“a”是绿色的,字符的大小也是如此,有的“a”是五号字,有的“a”是四 号字。而且字符的颜色和大小是两个独立的外部状态,它们可以独立变化,相互之间没有影 响,客户端可以在使用时将外部状态注入享元对象中。
正因为区分了内部状态和外部状态,我们可以将具有相同内部状态的对象存储在享元池中, 享元池中的对象是可以实现共享的,需要的时候就将对象从享元池中取出,实现对象的复 用。通过向取出的对象注入不同的外部状态,可以得到一系列相似的对象,而这些对象在内 存中实际上只存储一份。
享元模式定义如下:
享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度对象的复用。系统只使 用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于 享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种 对象结构型模式。
完整解决方案
为了节约存储空间,提高系统性能,Sunny公司开发人员使用享元模式来设计围棋软件中的棋 子,其基本结构如图
,
IgoChessman充当抽象享元类,BlackIgoChessman和WhiteIgoChessman充当具体享 元类,IgoChessmanFactory充当享元工厂类。完整代码如下所示:
import java.util.*; //围棋棋子类:抽象享元类 abstract class IgoChessman { public abstract String getColor(); public void display() { System.out.println("棋子颜色:" + this.getColor()); } } //黑色棋子类:具体享元类 class BlackIgoChessman extends IgoChessman { public String getColor() { return "黑色"; } } //白色棋子类:具体享元类 class WhiteIgoChessman extends IgoChessman { public String getColor() { return "白色"; } } //围棋棋子工厂类:享元工厂类,使用单例模式进行设计 class IgoChessmanFactory { private static IgoChessmanFactory instance = new IgoChessmanFactory(); private static Hashtable ht; //使用Hashtable来存储享元对象,充当享元池 private IgoChessmanFactory() { ht = new Hashtable(); IgoChessman black, white; black = new BlackIgoChessman(); ht.put("b", black); white = new WhiteIgoChessman(); ht.put("w", white); } //返回享元工厂类的唯一实例 public static IgoChessmanFactory getInstance() { return instance; } //通过key来获取存储在Hashtable中的享元对象 public static IgoChessman getIgoChessman(String color) { return (IgoChessman) ht.get(color); } } //编写如下客户端测试代码: class Client { public static void main(String args[]) { IgoChessman black1, black2, black3, white1, white2; IgoChessmanFactory factory; //获取享元工厂对象 factory = IgoChessmanFactory.getInstance(); //通过享元工厂获取三颗黑子 black1 = factory.getIgoChessman("b"); black2 = factory.getIgoChessman("b"); black3 = factory.getIgoChessman("b"); System.out.println("判断两颗黑子是否相同:" + (black1 == black2)); //通过享元工厂获取两颗白子 white1 = factory.getIgoChessman("w"); white2 = factory.getIgoChessman("w"); System.out.println("判断两颗白子是否相同:" + (white1 == white2)); //显示棋子 black1.display(); black2.display(); black3.display(); white1.display(); white2.display(); } } |
编译并运行程序,输出结果如下:
判断两颗黑子是否相同:true
判断两颗白子是否相同:true
棋子颜色:黑色
棋子颜色:黑色
棋子颜色:黑色
棋子颜色:白色
棋子颜色:白色
从输出结果可以看出,虽然我们获取了三个黑子对象和两个白子对象,但是它们的内存地址 相同,也就是说,它们实际上是同一个对象。在实现享元工厂类时我们使用了单例模式和简 单工厂模式,确保了享元工厂对象的唯一性,并提供工厂方法来向客户端返回享元对象。
在JAVA语言中,String类型就是使用了享元模式。String对象是final类型,对象一旦创建就不可改变。在JAVA中字符串常量都是存在常量池中的,JAVA会确保一个字符串常量在常量池中只有一个拷贝。String a="abc",其中"abc"就是一个字符串常量。
熟悉java的应该知道下面这个例子:
String a =
"hello"
;
String b =
"hello"
;
if
(a == b)
System.out.println(
"OK"
);
else
System.out.println(
"Error"
);
输出结果是:OK。可以看出if条件比较的是两a和b的地址,也可以说是内存空间
享元模式总结
当系统中存在大量相同或者相似的对象时,享元模式是一种较好的解决方案,它通过共享技 术实现相同或相似的细粒度对象的复用,从而节约了内存空间,提高了系统性能。相比其他 结构型设计模式,享元模式的使用频率并不算太高,但是作为一种以“节约内存,提高性能”为 出发点的设计模式,它在软件开发中还是得到了一定程度的应用。
1.主要优点
享元模式的主要优点如下:
(1) 可以极大减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而可以节 约系统资源,提高系统性能。
(2) 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同 的环境中被共享。
2.主要缺点 享元模式的主要缺点如下:
(1) 享元模式使得系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂 化。
(2) 为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使 得运行时间变长。
3.适用场景 在以下情况下可以考虑使用享元模式:
(1) 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
(2) 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
(3) 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源, 因此,应当在需要多次重复使用享元对象时才值得使用享元模式。