设计模式解密(19)- 享元模式
1、简介
定义:运用共享技术有效地支持大量细粒度的对象。
主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
本质:分离与共享(分离的是对象状态中变与不变的部分,共享的是对象中不变的部分)。
核心:享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池,用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。
英文:Flyweight
类型:结构型
2、单纯享元模式 && 复合享元模式
先引入两个概念“内蕴状态(Internal State)和外蕴状态(External State)”:
享元模式采用一个共享来避免大量拥有相同内容对象的开销。这种开销最常见、最直观的就是内存的损耗。
享元对象能做到共享的关键是区分内蕴状态(Internal State)和外蕴状态(External State)。
1、 内蕴状态(内部状态)是存储在享元对象内部的,并且是不会随环境的改变而有所不同。因此,一个享元可以具有内蕴状态并可以共享。
2、外蕴状态(外部状态)是随环境的改变而改变的、不可以共享的。享元对象的外蕴状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象内部。外蕴状态不可以影响享元对象的内蕴状态,它们是相互独立的。
享元模式可以分成单纯享元模式和复合享元模式两种形式↓↓↓↓↓↓
2-1 单纯享元模式
在单纯的享元模式中,所有的享元对象都是可以共享的。
(引)类图:
组成:
● 抽象享元(Flyweight)角色 :给出一个抽象接口,以规定出所有具体享元角色需要实现的方法。
● 具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定出的接口。如果有内蕴状态的话,必须负责为内蕴状态提供存储空间。
● 享元工厂(FlyweightFactory)角色 :负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象调用一个享元对象的时候,享元工厂角色会检查系统中是否已经有一个符合要求的享元对象。如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个合适的享元对象。
代码结构:
/*** * 享元接口,通过这个接口享元可以接受并作用于外蕴状态 */ public interface Flyweight { /** * 示例操作,传入外蕴状态 * @param extrinsicState 示例参数,外蕴状态 */ public void operation(String extrinsicState); } /** * 享元对象,封装flyweight的内蕴状态 */ public class ConcreteFlyweight implements Flyweight{ /** * 示例,描述内蕴状态 */ private String intrinsicState; /** * 构造方法,传入享元对象的内蕴状态 * @param state 享元对象的内蕴状态 */ public ConcreteFlyweight(String state){ this.intrinsicState = state; } /** * 外蕴状态作为参数传入方法中,改变方法的行为, * 但是并不改变对象的内蕴状态。 */ public void operation(String extrinsicState) { //具体的功能处理,可能会用到享元内蕴、外蕴状态 } } /** * 享元工厂 * 在享元模式中,客户端不能直接创建共享的享元对象实例,必须通过享元工厂来创建 */ public class FlyweightFactory { /** * 缓存多个flyweight对象,这里只是示意一下 */ private Map<String,Flyweight> fsMap = new HashMap<String,Flyweight>(); /** * 获取key对应的享元对象 * @param key 获取享元对象的key,只是示意 * @return key 对应的享元对象 */ public Flyweight factory(String key) { //这个方法里面基本的实现步骤如下: //1:先从缓存里面查找,是否存在key对应的Flyweight对象 Flyweight f = fsMap.get(key); //如果存在,就返回相对应的Flyweight对象 if(f == null){//如果不存在 //创建一个新的Flyweight对象 f = new ConcreteFlyweight(key); //把这个新的Flyweight对象添加到缓存里面 fsMap.put(key,f); } return f; } }
2-2 复合享元模式:
在单纯享元模式中,所有的享元对象都是单纯享元对象,也就是说都是可以直接共享的。还有一种较为复杂的情况,将一些单纯享元加以复合,形成复合享元对象。这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,其中单纯享元对象则可以共享。
(引)类图:
组成:
● 抽象享元(Flyweight)角色 :给出一个抽象接口,以规定出所有具体享元角色需要实现的方法。
● 具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定出的接口。如果有内蕴状态的话,必须负责为内蕴状态提供存储空间。
● 复合享元(ConcreteCompositeFlyweight)角色 :复合享元角色所代表的对象是不可以共享的,但是一个复合享元对象可以分解成为多个本身是单纯享元对象的组合。复合享元角色又称作不可共享的享元对象(UnsharedConcreteFlyweight)。
● 享元工厂(FlyweightFactory)角色 :负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象调用一个享元对象的时候,享元工厂角色会检查系统中是否已经有 一个符合要求的享元对象。如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个 合适的享元对象。 (这里与单纯享元不同的是,这里包含两个工厂方法,一个是单纯享元工厂方法,另一个是复合享元工厂方法,其中复合享元工厂方法内部调用的还是单纯享元工厂方法,复合享元工厂方法只是起到了组合的作用)
代码结构:
/*** * 享元接口,通过这个接口享元可以接受并作用于外蕴状态 */ public interface Flyweight { /** * 示例操作,传入外蕴状态 * @param extrinsicState 示例参数,外蕴状态 */ public void operation(String extrinsicState); } /** * 享元对象,封装flyweight的内蕴状态 */ public class ConcreteFlyweight implements Flyweight{ /** * 示例,描述内蕴状态 */ private String intrinsicState; /** * 构造方法,传入享元对象的内蕴状态 * @param state 享元对象的内蕴状态 */ public ConcreteFlyweight(String state){ this.intrinsicState = state; } /** * 外蕴状态作为参数传入方法中,改变方法的行为, * 但是并不改变对象的内蕴状态。 */ public void operation(String extrinsicState) { //具体的功能处理,可能会用到享元内蕴、外蕴状态 } } /** * 复合享元角色 */ public class ConcreteCompositeFlyweight implements Flyweight { private Map<String,Flyweight> flies = new HashMap<String,Flyweight>(); /** * 增加一个新的单纯享元对象到聚集中 */ public void add(String key , Flyweight fly){ flies.put(key,fly); } /** * 外蕴状态作为参数传入到方法中 */ @Override public void operation(String state) { Flyweight fly = null; for(Object o : flies.keySet()){ fly = flies.get(o); fly.operation(state); } } } /** * 享元工厂 * 在享元模式中,客户端不能直接创建共享的享元对象实例,必须通过享元工厂来创建 */ public class FlyweightFactory { private Map<String,Flyweight> flies = new HashMap<String,Flyweight>(); /** * 复合享元工厂方法 */ public Flyweight factory(List<String> compositeState){ ConcreteCompositeFlyweight compositeFly = new ConcreteCompositeFlyweight(); for(String state : compositeState){ compositeFly.add(state,this.factory(state)); } return compositeFly; } /** * 单纯享元工厂方法 */ public Flyweight factory(String state){ //先从缓存中查找对象 Flyweight fly = flies.get(state); if(fly == null){ //如果对象不存在则创建一个新的Flyweight对象 fly = new ConcreteFlyweight(state); //把这个新的Flyweight对象添加到缓存中 flies.put(state, fly); } return fly; } }
单纯享元模式与复合享元模式的区别:复合享元模式比单纯享元模式多了一个ConcreteCompositeFlyweight类,而这个类又是不可共享的,这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,其中单纯享元对象则可以共享。
3、实例讲解
这里模拟饭店客户点菜的情景(下面的例子外蕴状态可以看做一个点菜的人,内蕴状态可以看做一个菜名,点菜的人一般经常变,但是菜名是固定的):
单纯享元模式实现:
package com.designpattern.Flyweight.simple; /** * 享元接口 * @author Json<<json1990@foxmail.com>> */ public interface Flyweight { /** * 传入外部状态 * @param extrinsicState 外部状态 */ public void operation(String extrinsicState); }
package com.designpattern.Flyweight.simple; /** * 具体的享元对象 * @author Json<<json1990@foxmail.com>> */ public class ConcreteFlyweight implements Flyweight{ /** * 描述内部状态 */ private String intrinsicState; /** * 构造方法,传入享元对象的内部状态的数据 * @param state 享元对象的内部状态的数据 */ public ConcreteFlyweight(String state){ this.intrinsicState = state; } @Override public void operation(String extrinsicState) { //具体的功能处理,可能会用到享元内部、外部的状态 System.out.println("内蕴状态:"+this.intrinsicState+",外蕴状态:"+extrinsicState+""); } }
package com.designpattern.Flyweight.simple; import java.util.HashMap; import java.util.Map; /** * 享元工厂 * @author Json<<json1990@foxmail.com>> */ public class FlyweightFactory { /** * 缓存多个flyweight对象,这里只是示意一下 */ private Map<String,Flyweight> fsMap = new HashMap<String,Flyweight>(); /** * 获取key对应的享元对象 * @param key 获取享元对象的key,只是示意 * @return key 对应的享元对象 */ public Flyweight factory(String key) { //这个方法里面基本的实现步骤如下: //先从缓存里面查找,是否存在key对应的Flyweight对象 Flyweight f = fsMap.get(key); //如果存在,就返回相对应的Flyweight对象 if(f == null){ //如果不存在 //创建一个新的Flyweight对象 f = new ConcreteFlyweight(key); //把这个新的Flyweight对象添加到缓存里面 fsMap.put(key,f); } return f; } //获取flyweight对象个数 public int getFlyweightSize() { return fsMap.size(); } }
辅助类--菜单:
package com.designpattern.Flyweight.simple; /** * 点菜的菜单(简单模拟,列举几个常见的菜名) * @author Json<<json1990@foxmail.com>> */ public class Menu { public static String 鱼香肉丝 = "01"; public static String 酸辣土豆丝 = "02"; public static String 鱼香茄条 = "03"; public static String 酸菜鱼 = "04"; public static String 水煮肉片 = "05"; public static String 米饭 = "99"; }
测试:
package com.designpattern.Flyweight.simple; /** * 测试 * @author Json<<json1990@foxmail.com>> */ public class Client { public static void main(String[] args) { FlyweightFactory flyfactory = new FlyweightFactory(); Flyweight fly1 = flyfactory.factory(Menu.水煮肉片); fly1.operation("张三点菜"); Flyweight fly2 = flyfactory.factory(Menu.酸菜鱼); fly2.operation("李四点菜"); Flyweight fly3 = flyfactory.factory(Menu.酸辣土豆丝); fly3.operation("王五点菜"); Flyweight fly4 = flyfactory.factory(Menu.酸辣土豆丝); fly4.operation("刘六点菜"); Flyweight fly5 = flyfactory.factory(Menu.酸辣土豆丝); fly5.operation("赵七点菜"); System.out.println(flyfactory.getFlyweightSize());; } }
结果:
内蕴状态:05,外蕴状态:张三点菜 内蕴状态:04,外蕴状态:李四点菜 内蕴状态:02,外蕴状态:王五点菜 内蕴状态:02,外蕴状态:刘六点菜 内蕴状态:02,外蕴状态:赵七点菜 3
从结果看,客户端申请了5个享元对象,实际就生成了3个对象,这就是共享!!!
但是在实际的例子中,不可能只存在每个人只点一个菜的情况,还存在一个人点多个菜的情况,下面用复合享元模式重写一下实例:
复合享元模式(修改上面实例):
package com.designpattern.Flyweight.composite; /** * 享元接口 * @author Json<<json1990@foxmail.com>> */ public interface Flyweight { /** * 传入外部状态 * @param extrinsicState 外部状态 */ public void operation(String extrinsicState); }
package com.designpattern.Flyweight.composite; /** * 具体的享元对象 * @author Json<<json1990@foxmail.com>> */ public class ConcreteFlyweight implements Flyweight{ /** * 描述内部状态 */ private String intrinsicState; /** * 构造方法,传入享元对象的内部状态的数据 * @param state 享元对象的内部状态的数据 */ public ConcreteFlyweight(String state){ this.intrinsicState = state; } @Override public void operation(String extrinsicState) { //具体的功能处理,可能会用到享元内部、外部的状态 System.out.println("内蕴状态:"+this.intrinsicState+",外蕴状态:"+extrinsicState+""); } }
package com.designpattern.Flyweight.composite; import java.util.HashMap; import java.util.Map; /** * 复合享元角色 * @author Json<<json1990@foxmail.com>> */ public class ConcreteCompositeFlyweight implements Flyweight { private Map<String,Flyweight> flies = new HashMap<String,Flyweight>(); /** * 增加一个新的单纯享元对象到聚集中 */ public void add(String key , Flyweight fly){ flies.put(key,fly); } /** * 外蕴状态作为参数传入到方法中 */ @Override public void operation(String state) { Flyweight fly = null; for(Object o : flies.keySet()){ fly = flies.get(o); fly.operation(state); } } }
package com.designpattern.Flyweight.composite; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 享元工厂 * @author Json<<json1990@foxmail.com>> */ public class FlyweightFactory { private Map<String,Flyweight> flies = new HashMap<String,Flyweight>(); /** * 复合享元工厂方法 */ public Flyweight factory(List<String> compositeState){ ConcreteCompositeFlyweight compositeFly = new ConcreteCompositeFlyweight(); for(String state : compositeState){ compositeFly.add(state,this.factory(state)); } return compositeFly; } /** * 单纯享元工厂方法 */ public Flyweight factory(String state){ //先从缓存中查找对象 Flyweight fly = flies.get(state); if(fly == null){ //如果对象不存在则创建一个新的Flyweight对象 fly = new ConcreteFlyweight(state); //把这个新的Flyweight对象添加到缓存中 flies.put(state, fly); } return fly; } // 得到存对象的集合的长度 public int getFlyWeightSize() { return flies.size(); } }
package com.designpattern.Flyweight.composite; /** * 点菜的菜单(简单模拟,列举几个常见的菜名) * @author Json<<json1990@foxmail.com>> */ public class Menu { public static String 鱼香肉丝 = "01"; public static String 酸辣土豆丝 = "02"; public static String 鱼香茄条 = "03"; public static String 酸菜鱼 = "04"; public static String 水煮肉片 = "05"; public static String 米饭 = "99"; }
测试:
package com.designpattern.Flyweight.composite; import java.util.ArrayList; import java.util.List; /** * 测试 * @author Json<<json1990@foxmail.com>> */ public class Client { public static void main(String[] args) { List<String> compositeState = new ArrayList<String>(); compositeState.add(Menu.水煮肉片); compositeState.add(Menu.酸辣土豆丝); compositeState.add(Menu.米饭); FlyweightFactory flyFactory = new FlyweightFactory(); Flyweight compositeFly1 = flyFactory.factory(compositeState); compositeFly1.operation("张三点菜"); System.out.println(); List<String> compositeState2 = new ArrayList<String>(); compositeState2.add(Menu.鱼香茄条); compositeState2.add(Menu.鱼香肉丝); compositeState2.add(Menu.米饭); Flyweight compositeFly2 = flyFactory.factory(compositeState2); compositeFly2.operation("李四点菜"); System.out.println(); System.out.println("复合享元模式是否可以共享对象:" + (compositeFly1 == compositeFly2)); System.out.println(flyFactory.getFlyWeightSize()); } }
结果:
内蕴状态:05,外蕴状态:张三点菜
内蕴状态:99,外蕴状态:张三点菜
内蕴状态:02,外蕴状态:张三点菜
内蕴状态:99,外蕴状态:李四点菜
内蕴状态:01,外蕴状态:李四点菜
内蕴状态:03,外蕴状态:李四点菜
复合享元模式是否可以共享对象:false
5
从运行结果可以看出:
复合享元对象的所有单纯享元对象元素的外蕴状态都是与复合享元对象的外蕴状态相等的。即外运状态都等于张三点菜或者李四点菜。
复合享元对象所含有的单纯享元对象的内蕴状态一般是不相等的。即内蕴状态分别为各自的菜名。
复合享元对象是不能共享的。即使用相同的对象compositeState通过工厂分别两次创建出的对象不是同一个对象。
单纯享元对象是可以共享的。在结果中发现,一共生成5个对象,说明米饭(内蕴状态:99)是共享的。
4、模式深入讲解(引)
享元模式是一个考虑系统性能的设计模式,通过使用享元模式可以节约内存空间,提高系统的性能。
4-1 变与不变
享元模式设计的重点就在于分离变与不变,把一个对象的状态分成内蕴状态和外蕴状态,内蕴状态是不变的,外蕴状态是可变的。然后通过共享不变的部分,达到减少对象数量、并节约内存的目的。在享元对象需要的时候,可以从外部传入外蕴状态给共享的对象,共享对象会在功能处理的时候,使用到自己内蕴状态和这些外蕴状态。
事实上,分离变与不变是软件设计上最基本的方式之一,比如预留接口,为什么在这个地方要预留接口,一个常见的原因就是这里存在变化,可能在今后需要扩展、或者是改变已有的实现,因此预留接口做为“可插入性的保证”。
4-2 共享与不共享
在享元模式中,享元对象又有共享与不共享之分,即单纯享元与复合享元,通常共享的是叶子对象,一般不共享的部分是由共享部分组合而成的,由于所有细粒度的叶子对象都已经缓存了,所以就不需要缓存树枝节点了,也就是单纯享元是需要共享的,复合享元不需要共享。
4-3 实例池
在享元模式中,为了创建和管理共享的享元部分,引入了享元工厂,享元工厂中一般都包含有享元对象的实例池,享元对象就是缓存在这个实例池中的。
简单介绍一点实例池的知识,所谓实例池,指的是缓存和管理对象实例的程序,通常实例池会提供对象实例的运行环境,并控制对象实例的生命周期。
工业级的实例池实现上有两个最基本的难点,一个就是动态控制实例数量,一个就是动态分配实例来提供给外部使用。这些都是需要算法来做保证的。
假如实例池里面已有了3个实例,但是客户端请求非常多,有些忙不过来,那么实例池的管理程序就应该判断出来,到底几个实例才能满足现在的客户需求,理想状况是刚刚好,就是既能够满足应用的需要,又不会造成对象实例的浪费,假如经过判断5个实例正好,那么实例池的管理程序就应该能动态的创建2个新的实例。
这样运行了一段时间,客户端的请求减少了,这个时候实例池的管理程序又应该动态的判断,究竟几个实例是最好的,多了明显浪费资源,假如经过判断只需要1个实例就可以了,那么实例池的管理程序应该销毁掉多余的4个实例,以释放资源。这就是动态控制实例数量。
对于动态分配实例,也说明一下吧,假如实例池里面有3个实例,这个时候来了一个新的请求,到底调度哪一个实例去执行客户的请求呢,如果有空闲实例,那就是它了,要是没有空闲实例呢,是新建一个实例,还是等待运行中的实例,等它运行完了就来处理这个请求呢?具体如何调度,也是需要算法来保障的。
回到享元模式中来,享元工厂中的实例池可没有这么复杂,因为共享的享元对象基本上都是一个实例,一般不会出现同一个享元对象有多个实例的情况,这样就不用去考虑动态创建和销毁享元对象实例的功能;另外只有一个实例,也就不存在动态调度的麻烦,反正就是它了。
这也主要是因为享元对象封装的多半是对象的内部状态,这些状态通常是不变的,有一个实例就够了,不需要动态控制生命周期,也不需要动态调度,它只需要做一个缓存而已,没有上升到真正的实例池那么个高度。
5、优缺点
优点:
可共享相同或相似的细粒度对象,节约系统资源,提供系统性能,同时降低了对象创建与垃圾回收的开销。
享元模式中的外部状态相对独立,使得对象可以在不同的环境中被复用(共享对象可以适应不同的外部环境),且不影响内部状态。
缺点:
外部状态由客户端保存,共享对象读取外部状态的开销可能比较大。
享元模式要求将内部状态与外部状态分离,这使得程序的逻辑复杂化,同时也增加了状态维护成本,使程序逻辑复杂。
6、应用场景
1、如果一个应用程序使用了大量的细粒度对象,可以使用享元模式来减少对象数量;
2、如果由于使用大量的对象,这些对象消耗大量内存,可以使用享元模式来减少对象数量,并节约内存;
3、如果对象的大多数状态都可以转变为外部状态,比如通过计算得到,或是从外部传入等,可以使用享元模式来实现内部状态和外部状态的分离;
4、这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替;
5、系统不依赖于这些对象身份,这些对象是不可分辨的;
6、享元模式一般是给出本地内存资源节省的一个方案,并不适合互联网上的分布式应用的情况,不过享元模式对于排他性的要求资源的控制,是个不错的选择的;
.......
7、JDK中的享元模式
举例说一下,这里以String为例:
public class Test { public static void main(String[] args){ String n = "Java"; String m = "Java"; System.out.println(n==m); } }
这段代码会告诉你 n==m 是 true,这就说明了在JVM中n和m两个引用了同一个String对象。
String类型的设计避免了在创建N多的String对象时产生的不必要的资源损耗,可以说是享元模式应用的范例!
这里只是简单举例,我会在以后写一篇关于String源码的解析文章,这里就不再深入讲解了,请稍后关注!
另外java中还有很多地方用到,例如:Byte、Short、Integer、Long、Character ,详见我的另一篇文章:http://www.cnblogs.com/JsonShare/p/7009028.html
8、总结
享元模式是一个考虑系统性能的设计模式,通过使用享元模式可以节约内存空间,提高系统的性能。
当系统中存在大量相同或者相似的对象时,享元模式是一种较好的解决方案,它通过共享技术实现相同或相似的细粒度对象的复用,从而节约了内存空间,提高了系统性能。
相比其他结构型设计模式,享元模式的使用频率并不算太高,但是作为一种以“节约内存,提高性能”为出发点的设计模式,它在软件开发中还是得到了一定程度的应用。
PS:源码地址 https://github.com/JsonShare/DesignPattern/tree/master
PS:原文地址 http://www.cnblogs.com/JsonShare/p/7338419.html