享元对象: 内部状态 保存在享元池, 外部状态,客户端使用时设置。 存储在享元池中 键值对集合 结合工厂模式 实现对象的共享。
重点在维护一个享元池, 然后外部状态的传入。
享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。
内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,内部状态可以共享。
外部状态是随环境改变而改变的、不可以共享的状态。
我们可以将具有相同内部状态的对象存储在享元池中,享元池中的对象是可以实现共享的,需要的时候就将对象从享元池中取出,实现对象的复用。通过向取出的对象注入不同的外部状态,可以得到一系列相似的对象,而这些对象在内存中实际上只存储一份。
享元模式结构较为复杂,一般结合工厂模式一起使用,在它的结构图中包含了一个享元工厂类
享元池一般设计为一个存储“键值对”的集合(也可以是其他类型的集合),它针对抽象享元类编程。
class FlyweightFactory { //定义一个HashMap用于存储享元对象,实现享元池 private HashMap flyweights = newHashMap(); public Flyweight getFlyweight(String key){ //如果对象存在,则直接从享元池获取 if(flyweights.containsKey(key)){ return(Flyweight)flyweights.get(key); } //如果对象不存在,先创建一个新的对象添加到享元池中,然后返回 else { Flyweight fw = newConcreteFlyweight(); flyweights.put(key,fw); return fw; } } } class Flyweight { //内部状态intrinsicState作为成员变量,同一个享元对象其内部状态是一致的 private String intrinsicState; public Flyweight(String intrinsicState) { this.intrinsicState=intrinsicState; } //**********外部状态extrinsicState在使用时由外部设置,不保存在享元对象中,即使是同一个对象,在每一次调用时也可以传入不同的外部状态 public void operation(String extrinsicState) { ...... } }
//围棋棋子工厂类:享元工厂类,使用单例模式进行设计 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 Coordinates { private int x; private int y; public Coordinates(int x,int y) { this.x = x; this.y = y; } public int getX() { return this.x; } public void setX(int x) { this.x = x; } public int getY() { return this.y; } public void setY(int y) { this.y = y; } } //围棋棋子类:抽象享元类 abstract class IgoChessman { public abstract String getColor(); public void display(Coordinates coord){ System.out.println("棋子颜色:" + this.getColor() + ",棋子位置:" + coord.getX() + "," + coord.getY() ); } } 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(new Coordinates(1,2)); black2.display(new Coordinates(3,4)); black3.display(new Coordinates(1,3)); white1.display(new Coordinates(2,5)); white2.display(new Coordinates(2,4)); } }
1.单纯享元模式
在单纯享元模式中,所有的具体享元类都是可以共享的,不存在非共享具体享元类。单纯享元模式的结构如图14-6所示:
2.复合享元模式
将一些单纯享元对象使用组合模式加以组合,还可以形成复合享元对象,这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,而后者则可以共享。复合享元模式的结构如图14-7所示:
图14-7 复合享元模式结构图
通过复合享元模式,可以确保复合享元类CompositeConcreteFlyweight中所包含的每个单纯享元类ConcreteFlyweight都具有相同的外部状态,而这些单纯享元的内部状态往往可以不同。如果希望为多个内部状态不同的享元对象设置相同的外部状态,可以考虑使用复合享元模式。
1.与其他模式的联用
享元模式通常需要和其他模式一起联用,几种常见的联用方式如下:
(1)在享元模式的享元工厂类中通常提供一个静态的工厂方法用于返回享元对象,使用简单工厂模式来生成享元对象。
(2)在一个系统中,通常只有唯一一个享元工厂,因此可以使用单例模式进行享元工厂类的设计。
(3)享元模式可以结合组合模式形成复合享元模式,统一对多个享元对象设置外部状态。
2.享元模式与String类
JDK类库中的String类使用了享元模式
关于Java String类这种在修改享元对象时,先将原有对象复制一份,然后在新对象上再实施修改操作的机制称为“Copy On Write”,大家可以自行查询相关资料来进一步了解和学习“Copy On Write”机制,在此不作详细说明。
在以下情况下可以考虑使用享元模式:
(1) 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
(2) 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
(3) 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。
练习
Sunny软件公司欲开发一个多功能文档编辑器,在文本文档中可以插入图片、动画、视频等多媒体资料,为了节约系统资源,相同的图片、动画和视频在同一个文档中只需保存一份,但是可以多次重复出现,而且它们每次出现时位置和大小均可不同。试使用享元模式设计该文档编辑器。