享元模式 FlyWeight 结构型 设计模式(十五)
享元模式(FlyWeight)
“享”取“共享”之意,“元”取“单元”之意。
意图
运用共享技术,有效的支持大量细粒度的对象。
意图解析
面向对象的程序设计中,一切皆是对象,这也就意味着系统的运行将会依赖大量的对象。
试想,如果对象的数量过多,势必会增加系统负担,导致运行的代价过高。
下面看两个小例子理解下
1.)有一首歌曲叫做《大舌头》
其中有一句歌词“说说说说 说你爱我 我我我我 说不出口”
2.)有一个文本编辑器软件,对于每一个字符使用对象进行表示
当打开一篇有很多重复字符的、数万字的文章时,你会使用几个对象进行表示?
如果仍旧采用每个字符占用一个对象,系统势必崩溃,必然需要共享对象
上面的两个例子中,都涉及到重复对象的概念
而享元模式的意图就是如此,将重复的对象进行共享以达到支持大量细粒度对象的目的。
如果不进行共享,如例2中描述的那样,一篇数万字符的文章将会产生数万个对象,这将是一场可怕的灾难。
flyweight意为轻量级
在我们当前的场景下,寓意为通过共享技术,轻量级的---也就是内存占用更小
本质就是“共享”所以中文翻译过来多称之为享元
简言之,享元模式就是要“共享对象”
对于Java语言,我们熟悉的String,就是享元模式的运用
String是不可变对象,一旦创建,将不会改变
在JVM内部,String对象都是共享的
如果一个系统中的两个String对象,包含的字符串相同,只会创建一个String对象提供给两个引用
从而实现String对象的共享(new 的对象是两个不同的)
享元模式又不仅仅是简单的“共享对象”
上面的两个小例子中,对于文字中的重复字符
可以通过共享对象的方式,对某些对象进行共享,从而减少内存开销。
考虑下图中的情景,这里面所有的“你”字,到底是不是同样的?- 是,因为全部都是汉字“你”
- 不是,因为尽管都是汉字“你”,但是他们的字体,颜色,字号,却又明显不同,所以不是同样的
如果将字体、颜色、字号,作为“你”这个汉字的状态
是不是可以认为:他们都是一样的汉字,但是他们却又具有不同的状态?
其实享元模式不仅仅用来解决大量重复对象的共享问题,还能够用来解决相似对象的问题。
享元对象能够共享的关键在于:区分对象的内部状态和外部状态
内部状态是存储在享元对象内部的,并且不会随环境的变化而有所改变。
比如上面的汉字“你”,无论在任何情况下,汉字“你”,始终是“你”,不会变成“她”
所以说享元模式解决共享问题,本质是共享内部状态
外部状态是随外部环境变化而变化,不能共享的状态。
享元对象的外部状态通常由客户端保存,在必要的时候在传递到享元对象内部
比如上面汉字“你”的字体、颜色、字号就是外部状态。
小结
享元模式就是为了避免系统中出现大量相同或相似的对象,同时又不影响客户端程序通过面向对象的方式对这些对象进行操作
享元模式通过共享技术,实现相同或相似对象的重用
比如文编编辑器读取文本
在逻辑上每一个出现的字符都有一个对象与之对应,然而在物理上它们却共享同一个享元对象
在享元模式中,存储这些共享实例对象的地方通常叫做享元池(Flyweight Pool)
享元模式可以结合String的intern()方法一起进行理解
通过区分了内部状态和外部状态,就可以将相同内部状态的对象存储在池中,池中的对象可以实现共享
需要的时候将对象从池中取出,实现对象的复用
通过向取出的对象注入不同的外部状态,进而得到一些列相似的对象
而这些看似各异的对象在内存中,仅仅存储了一份,大大节省了空间,所以说很自然的命名为“flyweight”轻量级
享元工厂
通过对意图的认识,可以认为,享元模式其实就是对于“程序中会出现的大量重复或者相似对象”的一种“重构”
当然,你应该是在设计之初就想到这个问题,而不是真的出现问题后再去真的重构
比如,你想要设计“字符”这种对象时,就应该考虑到他的“大量””重复““相似”的特点
所以需要分析出字符的内部状态,与外部状态
上面也提到对于享元对象,通过享元池进行管理
对于池的管理通常使用工厂模式,借助于工厂类对享元池进行管理
用户需要对象时,通过工厂类获取
工厂提供一个存储在享元池中的已创建的对象实例,或者创建一个新的实例
示例代码
针对于上面的例子,汉字“你”作为内部状态,可以进行共享
“颜色”作为外部状态,由客户端保存传递
创建字符类 Character、汉字字符类ChineseCharacter、颜色类Color以及工厂类CharacterFactory
Color含有颜色属性,通过构造方法设置,getter方法获取
package flyweight; public class Color { public String Color; public Color(String color) { this.Color = color; } public String getColor() { return Color; } }
Character 抽象的字符类,用于描述字符
package flyweight; public abstract class Character { public abstract String getValue(); public void display(Color color) { System.out.println("字符: " + getValue() + " ,颜色: " + color.getColor()); } }
汉字字符类,为了简化,直接设置value为汉字“你”
package flyweight; public class ChineseCharacter extends Character { @Override public String getValue() { return "你"; } }
CharacterFactory字符工厂类
通过单例模式创建工厂
内部HashMap用于存储字符,并且提供获取方法
为了简化程序,初始就创建了一个汉字字符“你”存储于字符中
package flyweight; import java.util.HashMap; public class CharacterFactory { /** * 单例模式 饿汉式创建 */ private static CharacterFactory instance = new CharacterFactory(); /** * 使用HashMap管理享元池 */ private HashMap<String, Object> hm = new HashMap<>(); private CharacterFactory() { Character character = new ChineseCharacter(); hm.put("你", character); } /** * 单例全局访问接口获取工厂 */ public static CharacterFactory getInstance() { return instance; } /** * 根据key获取池中的对象 */ public Character getCharacter(String key) { return (Character) hm.get(key); } }
测试代码
示例中,我们通过工厂,从享元池中获取了三个汉字字符“你”。
通过 == 可以看得出来,他们都是同一个对象
在分别调用他们的display方法时,在客户端(此处为我们的Test main方法)中创建,并且传递给享元对象
通过方法参数的形式进行外部状态的设置。
CharacterFactory 单例模式,返回自身实例
CharacterFactory内部维护Character的享元池
Character 依赖Color
ChineseCharacter是Character的实现类
结构
将上面的示例转换为标准的享元模式的名称
抽象享元角色 FlyWeight
所有具体享元类的超类,为这些类规定了需要实现的公共接口
外部状态可以通过业务逻辑方法的参数形式传递进来
具体享元角色ConcreteFlyWeight
实现抽象享元角色所规定的的接口
需要保存内部状态,而且,内部状态必须与外部状态无关
从而才能使享元对象可以在系统内共享
享元工厂角色 FlyWeightFactory
负责创建和管理享元角色,也就是维护享元池
必须保证享元对象可以被系统适当的共享
接受客户端的请求
如果有适当符合要求的享元对象,则返回
如果没有一个适当的享元对象,则创建
客户端角色维护了对所有享元对象的引用
需要保存维护享元对象的外部状态,然后通过享元对象的业务逻辑方法作为参数形式传递
分类
单纯享元模式
在上面的结构中,如果所有的ConcreteFlyWeight都可以被共享
也就是所有的FlyWeight子类都可以被共享,那就是所有的享元对象都可以被共享
这种形式被称之为单纯享元模式
单纯享元代码
package flyweight.simple; public abstract class FlyWeight { /** * 抽象的业务逻辑方法,接受外部状态作为参数 */ abstract public void operation(String outerState); }
package flyweight.simple; public class ConcreteFlyWeight extends FlyWeight { private String innerState = null; public ConcreteFlyWeight(String innerState) { this.innerState = innerState; } /** * 外部状态作为参数传递 */ @Override public void operation(String outerState) { System.out.println("innerState = " + innerState + " outerState = " + outerState); } }
package flyweight.simple; import java.util.HashMap; public class FlyWeightFactory { /** * 单例模式 饿汉式创建 */ private static FlyWeightFactory instance = new FlyWeightFactory(); /** * 使用HashMap管理享元池 */ private HashMap<String, Object> hm = new HashMap<>(); private FlyWeightFactory() { } /** * 单例全局访问接口获取工厂 */ public static FlyWeightFactory getInstance() { return instance; } /** * 根据innerState获取池中的对象 * 存在返回,不存在创建并返回 */ public FlyWeight getFylWeight(String innerState) { if(hm.containsKey(innerState)){ return (FlyWeight) hm.get(innerState); }else{ FlyWeight flyWeight = new ConcreteFlyWeight(innerState); hm.put(innerState,flyWeight); return flyWeight; } } }
package flyweight.simple; public class Test { public static void main(String[] args){ FlyWeightFactory flyWeightFactory = FlyWeightFactory.getInstance(); FlyWeight flyWeight1 = flyWeightFactory.getFylWeight("First"); FlyWeight flyWeight2 = flyWeightFactory.getFylWeight("Second"); FlyWeight flyWeight3 = flyWeightFactory.getFylWeight("First"); System.out.println(flyWeight1); System.out.println(flyWeight2); System.out.println(flyWeight3); System.out.println(); flyWeight1.operation("outer state XXX"); flyWeight2.operation("outer state YYY"); flyWeight3.operation("outer state ZZZ"); } }
复合享元模式
与单纯享元模式对应的是复合享元模式
单纯享元模式中,所有的享元对象都可以共享
复合享元模式中,则并不是所有的ConcreteFlyWeight都可以被共享
也就是说:不是所有的享元对象都可以被共享
实际上,并不是所有的FlyWeight子类都需要被共享
FlyWeight接口使的可以进行共享,但是没有任何必要 强制必须共享
实践中,UnsharedConcreteFlyWeight对象通常将ConcreteFlyWeight对象作为子节点
与单纯享元模式相比,仅仅是拥有了不可共享的具体子类
而且,这个子类往往是应用了组合模式,将ConcreteFlyWeight对象作为子节点
复合享元角色UnsharedConcreteFlyWeight复合享元角色,也就是不可共享的,也被称为 不可共享的享元对象
但是一个复合享元对象可以分解为多个本身是单纯享元对象的组合
这些单纯的享元对象就又是可以共享的
复合享元代码
将简单模式中的示例代码进行改造
FlyWeight不变
package flyweight.composite; public abstract class FlyWeight { /** * 抽象的业务逻辑方法,接受外部状态作为参数 */ abstract public void operation(String outerState); }
ConcreteFlyWeight不变
package flyweight.composite; public class ConcreteFlyWeight extends FlyWeight { private String innerState = null; public ConcreteFlyWeight(String innerState) { this.innerState = innerState; } /** * 外部状态作为参数传递 */ @Override public void operation(String outerState) { System.out.println("innerState = " + innerState + " outerState = " + outerState); } }
新增加不共享的子类也就是组合的享元子类
内部使用list 维护单纯享元模式对象,提供add方法进行添加
提供operation操作
package flyweight.composite; import java.util.ArrayList; import java.util.List; public class UnsharedConcreateFlyWeight extends FlyWeight { private String innerState = null; public UnsharedConcreateFlyWeight(String innerState) { this.innerState = innerState; } private List<FlyWeight> list = new ArrayList<>(); public void add(FlyWeight flyWeight) { list.add(flyWeight); } @Override public void operation(String outerState) { for (FlyWeight flyWeight:list) { flyWeight.operation(outerState); } } }
FlyWeightFactory工厂类进行改造
新增加public UnsharedConcreateFlyWeight getCompositeFylWeight(String state)
用于获得组合享元对象
package flyweight.composite; import java.util.HashMap; public class FlyWeightFactory { /** * 单例模式 饿汉式创建 */ private static FlyWeightFactory instance = new FlyWeightFactory(); /** * 使用HashMap管理享元池 */ private HashMap<String, Object> hm = new HashMap<>(); /** * 管理复合享元对象 */ private HashMap<String, Object> compositeHm = new HashMap<>(); private FlyWeightFactory() { } /** * 单例全局访问接口获取工厂 */ public static FlyWeightFactory getInstance() { return instance; } /** * 根据innerState获取池中的对象 * 存在返回,不存在创建并返回 */ public FlyWeight getFylWeight(String innerState) { if(hm.containsKey(innerState)){ return (FlyWeight) hm.get(innerState); }else{ FlyWeight flyWeight = new ConcreteFlyWeight(innerState); hm.put(innerState,flyWeight); return flyWeight; } } /** * 根据innerState获取池中的对象 * 存在返回,不存在创建并返回 */ public UnsharedConcreateFlyWeight getCompositeFylWeight(String state) { if(compositeHm.containsKey(state)){ return (UnsharedConcreateFlyWeight) compositeHm.get(state); }else{ UnsharedConcreateFlyWeight flyWeight = new UnsharedConcreateFlyWeight(state); compositeHm.put(state,flyWeight); return flyWeight; } } }
测试类也进行改造
package flyweight.composite; public class Test { public static void main(String[] args){ FlyWeightFactory flyWeightFactory = FlyWeightFactory.getInstance(); FlyWeight flyWeight1 = flyWeightFactory.getFylWeight("First"); FlyWeight flyWeight2 = flyWeightFactory.getFylWeight("Second"); FlyWeight flyWeight3 = flyWeightFactory.getFylWeight("First"); System.out.println(flyWeight1); System.out.println(flyWeight2); System.out.println(flyWeight3); System.out.println("###########################################"); flyWeight1.operation("outer state XXX"); flyWeight2.operation("outer state YYY"); flyWeight3.operation("outer state ZZZ"); System.out.println("###########################################"); UnsharedConcreateFlyWeight compositeFlyWeight = flyWeightFactory.getCompositeFylWeight("composite"); compositeFlyWeight.add(flyWeight1); compositeFlyWeight.add(flyWeight2); compositeFlyWeight.operation("composite out state OOO"); } }
测试程序在原来的基础上,新获得了一个组合享元对象
然后将两个单纯享元对象添加到组合享元对象中
然后调用operation,通过打印信息可以看得出来
不同的单纯享元对象,他们却有了一致的外部状态
所以使用复合享元模式的一个常用目的就是:
多个内部状态不同的单纯享元对象,拥有一致的外部状态
这种场景下,就可以考虑使用复合享元模式
使用场景
如果有下列情况,则可以考虑使用享元模式
- 应用程序中使用了大量的对象
- 大量的对象明显增加了程序的存储运行开销
- 对象可以提取出内部状态,并且可以分离外部状态
使用享元模式有一点需要特别注意:应用程序运行不依赖这些对象的身份
换句话说这些对象是不做区分的,适用于“在客户端眼里,他们都是一样的”这种场景
比如单纯的使用对象的方法,而不在意对象是否是创建而来的,否则如果客户端鉴别对象的身份(equals),当他们是同一个对象时将会出现问题
总结
享元模式的核心就是共享
共享就需要找准内部状态,以及分离外部状态,外部状态由客户端维护,在必要时候,通过参数的形式注入到享元对象中
在有大量重复或者相似对象的场景下,都可以考虑到享元模式
而且为了达到共享的目的,需要通过工厂对象进行控制
只有通过工厂来维护享元池才能达到共享的目的,如果任意创建使用则势必不能很好地共享
享元模式大大的减少了对象的创建,降低了系统所需要的内存空间
但是由于将状态分为内部状态和外部状态,而外部状态是分离的,那么状态的读取必然会增大开销
所以说享元模式是时间换空间
如果确定需要使用享元模式,如果对于多个内部状态不同的享元对象,希望他们拥有一致性的外部状态
那么就可以考虑复合享元模式,复合享元模式是与合成模式的结合。