天天看点

设计模式(11)--Flyweight(享元模式)--结构型

享元模式是对象的结构模式。享元模式以共享的方式高效地支持大量的细粒度对象。

作者QQ:1095737364    QQ群:123300273     欢迎加入!

1.模式定义:

  享元模式是对象的结构模式。享元模式以共享的方式高效地支持大量的细粒度对象。

2.模式特点:

  享元模式是一个考虑系统性能的设计模式,通过使用享元模式可以节约内存空间,提高系统的性能。

  当系统中存在大量相同或者相似的对象时,享元模式是一种较好的解决方案,它通过共享技术实现相同或相似的细粒度对象的复用,从而节约了内存空间,提高了系统性能。

  相比其他结构型设计模式,享元模式的使用频率并不算太高,但是作为一种以“节约内存,提高性能”为出发点的设计模式,它在软件开发中还是得到了一定程度的应用

  享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池,用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。

3.使用场景:

  1、如果一个应用程序使用了大量的细粒度对象,可以使用享元模式来减少对象数量;

  2、如果由于使用大量的对象,这些对象消耗大量内存,可以使用享元模式来减少对象数量,并节约内存;

  3、如果对象的大多数状态都可以转变为外部状态,比如通过计算得到,或是从外部传入等,可以使用享元模式来实现内部状态和外部状态的分离;

  4、这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替;

  5、系统不依赖于这些对象身份,这些对象是不可分辨的;

  6、享元模式一般是给出本地内存资源节省的一个方案,并不适合互联网上的分布式应用的情况,不过享元模式对于排他性的要求资源的控制,是个不错的选择的;

4.模式实现:

  享元模式采用一个共享来避免大量拥有相同内容对象的开销。这种开销最常见、最直观的就是内存的损耗。享元对象能做到共享的关键是区分内蕴状态(Internal State)和外蕴状态(External State)。

  一个内蕴状态是存储在享元对象内部的,并且是不会随环境的改变而有所不同。因此,一个享元可以具有内蕴状态并可以共享。

  一个外蕴状态是随环境的改变而改变的、不可以共享的。享元对象的外蕴状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象内部。外蕴状态不可以影响享元对象的内蕴状态,它们是相互独立的。

  享元模式可以分成单纯享元模式和复合享元模式两种形式。

  (1)单纯享元模式  

    在单纯的享元模式中,所有的享元对象都是可以共享的。

设计模式(11)--Flyweight(享元模式)--结构型

 单纯享元模式所涉及到的角色如下:

    [1]抽象享元(Flyweight)角色 :

  给出一个抽象接口,以规定出所有具体享元角色需要实现的方法。

public interface Flyweight {
    //一个示意性方法,参数state是外蕴状态
    public void operation(String state);
}      

    [2]具体享元(ConcreteFlyweight)角色:

  实现抽象享元角色所规定出的接口。如果有内蕴状态的话,必须负责为内蕴状态提供存储空间。

public class ConcreteFlyweight implements Flyweight {
    private Character intrinsicState = null;
    /**
     * 构造函数,内蕴状态作为参数传入
     * @param state
     */
    public ConcreteFlyweight(Character state){
        this.intrinsicState = state;
    }    
    /**
     * 外蕴状态作为参数传入方法中,改变方法的行为,
     * 但是并不改变对象的内蕴状态。
     */
    @Override
    public void operation(String state) {
        // TODO Auto-generated method stub
        System.out.println("Intrinsic State = " + this.intrinsicState);
        System.out.println("Extrinsic State = " + state);
    }
}      

  具体享元角色类ConcreteFlyweight有一个内蕴状态,在本例中一个Character类型的intrinsicState属性代表,它的值应当在享元对象被创建时赋予。所有的内蕴状态在对象创建之后,就不会再改变了。

  如果一个享元对象有外蕴状态的话,所有的外部状态都必须存储在客户端,在使用享元对象时,再由客户端传入享元对象。这里只有一个外蕴状态,operation()方法的参数state就是由外部传入的外蕴状态。

    [3]享元工厂(FlyweightFactory)角色 :

  本角色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象调用一个享元对象的时候,享元工厂角色会检查系统中是否已经有一个符合要求的享元对象。如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个合适的享元对象。

public class FlyweightFactory {
    private Map<Character,Flyweight> files = new HashMap<Character,Flyweight>();    
    public Flyweight factory(Character state){
        //先从缓存中查找对象
        Flyweight fly = files.get(state);
        if(fly == null){
            //如果对象不存在则创建一个新的Flyweight对象
            fly = new ConcreteFlyweight(state);
            //把这个新的Flyweight对象添加到缓存中
            files.put(state, fly);
        }
        return fly;
    }
}      

  享元工厂角色类,必须指出的是,客户端不可以直接将具体享元类实例化,而必须通过一个工厂对象,利用一个factory()方法得到享元对象。一般而言,享元工厂对象在整个系统中只有一个,因此也可以使用单例模式。

  当客户端需要单纯享元对象的时候,需要调用享元工厂的factory()方法,并传入所需的单纯享元对象的内蕴状态,由工厂方法产生所需要的享元对象。

    [4]客户端类

public class Client {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        FlyweightFactory factory = new FlyweightFactory();
        Flyweight fly = factory.factory(new Character('a'));
        fly.operation("First Call");        
        fly = factory.factory(new Character('b'));
        fly.operation("Second Call");        
        fly = factory.factory(new Character('a'));
        fly.operation("Third Call");
    }
}      

    

  虽然客户端申请了三个享元对象,但是实际创建的享元对象只有两个,这就是共享的含义。运行结果如下:

设计模式(11)--Flyweight(享元模式)--结构型

  (2)复合享元模式

    在单纯享元模式中,所有的享元对象都是单纯享元对象,也就是说都是可以直接共享的。还有一种较为复杂的情况,将一些单纯享元使用合成模式加以复合,形成复合享元对象。这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,而后者则可以共享。

设计模式(11)--Flyweight(享元模式)--结构型

复合享元角色所涉及到的角色如下:

public interface Flyweight {
    //一个示意性方法,参数state是外蕴状态
    public void operation(String state);
}      

      [2]具体享元(ConcreteFlyweight)角色:

public class ConcreteFlyweight implements Flyweight {
    private Character intrinsicState = null;
    /**
     * 构造函数,内蕴状态作为参数传入
     * @param state
     */
    public ConcreteFlyweight(Character state){
        this.intrinsicState = state;
    }
    /**
     * 外蕴状态作为参数传入方法中,改变方法的行为,
     * 但是并不改变对象的内蕴状态。
     */
    @Override
    public void operation(String state) {
        // TODO Auto-generated method stub
        System.out.println("Intrinsic State = " + this.intrinsicState);
        System.out.println("Extrinsic State = " + state);
    }
}      

   复合享元对象是由单纯享元对象通过复合而成的,因此它提供了add()这样的聚集管理方法。由于一个复合享元对象具有不同的聚集元素,这些聚集元素在复合享元对象被创建之后加入,这本身就意味着复合享元对象的状态是会改变的,因此复合享元对象是不能共享的。

    [3]复合享元(ConcreteCompositeFlyweight)角色 :

  复合享元角色所代表的对象是不可以共享的,但是一个复合享元对象可以分解成为多个本身是单纯享元对象的组合。复合享元角色又称作不可共享的享元对象。

public class ConcreteCompositeFlyweight implements Flyweight {    
    private Map<Character,Flyweight> files = new HashMap<Character,Flyweight>();
    /**
     * 增加一个新的单纯享元对象到聚集中
     */
    public void add(Character key , Flyweight fly){
        files.put(key,fly);
    }
    /**
     * 外蕴状态作为参数传入到方法中
     */
    @Override
    public void operation(String state) {
        Flyweight fly = null;
        for(Object o : files.keySet()){
            fly = files.get(o);
            fly.operation(state);
        }        
    }
}      

  复合享元角色实现了抽象享元角色所规定的接口,也就是operation()方法,这个方法有一个参数,代表复合享元对象的外蕴状态。一个复合享元对象的所有单纯享元对象元素的外蕴状态都是与复合享元对象的外蕴状态相等的;而一个复合享元对象所含有的单纯享元对象的内蕴状态一般是不相等的,不然就没有使用价值了。

    [4]享元工厂(FlyweightFactory)角色 :

   本角 色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象调用一个享元对象的时候,享元工厂角色会检查系统中是否已经有 一个符合要求的享元对象。如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个 合适的享元对象。

public class FlyweightFactory {
    private Map<Character,Flyweight> files = new HashMap<Character,Flyweight>();
    /**
     * 复合享元工厂方法
     */
    public Flyweight factory(List<Character> compositeState){
        ConcreteCompositeFlyweight compositeFly = new ConcreteCompositeFlyweight();
        
        for(Character state : compositeState){
            compositeFly.add(state,this.factory(state));
        }
        
        return compositeFly;
    }
    /**
     * 单纯享元工厂方法
     */
    public Flyweight factory(Character state){
        //先从缓存中查找对象
        Flyweight fly = files.get(state);
        if(fly == null){
            //如果对象不存在则创建一个新的Flyweight对象
            fly = new ConcreteFlyweight(state);
            //把这个新的Flyweight对象添加到缓存中
            files.put(state, fly);
        }
        return fly;
    }
}      

  享元工厂角色提供两种不同的方法,一种用于提供单纯享元对象,另一种用于提供复合享元对象。

 [5]客户端角色

public class Client {
    public static void main(String[] args) {
        List<Character> compositeState = new ArrayList<Character>();
        compositeState.add('a');
        compositeState.add('b');
        compositeState.add('c');
        compositeState.add('a');
        compositeState.add('b');        
        FlyweightFactory flyFactory = new FlyweightFactory();
        Flyweight compositeFly1 = flyFactory.factory(compositeState);
        Flyweight compositeFly2 = flyFactory.factory(compositeState);
        compositeFly1.operation("Composite Call");       
        System.out.println("---------------------------------");        
        System.out.println("复合享元模式是否可以共享对象:" + (compositeFly1 == compositeFly2));        
        Character state = 'a';
        Flyweight fly1 = flyFactory.factory(state);
        Flyweight fly2 = flyFactory.factory(state);
        System.out.println("单纯享元模式是否可以共享对象:" + (fly1 == fly2));
    }
}      

运行结果如下:

设计模式(11)--Flyweight(享元模式)--结构型

   从运行结果可以看出,一个复合享元对象的所有单纯享元对象元素的外蕴状态都是与复合享元对象的外蕴状态相等的。即外运状态都等于Composite Call。

     从运行结果可以看出,一个复合享元对象所含有的单纯享元对象的内蕴状态一般是不相等的。即内蕴状态分别为b、c、a。

     从运行结果可以看出,复合享元对象是不能共享的。即使用相同的对象compositeState通过工厂分别两次创建出的对象不是同一个对象。

     从运行结果可以看出,单纯享元对象是可以共享的。即使用相同的对象state通过工厂分别两次创建出的对象是同一个对象。

    单纯享元模式与复合享元模式的区别:复合享元模式比单纯享元模式多了一个ConcreteCompositeFlyweight类,而这个类又是不可共享的,这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,其中单纯享元对象则可以共享。

5.优缺点:

  (1)享元模式优点

[1]大大减少对象的创建,降低系统的内存,使效率提高。

[2]可共享相同或相似的细粒度对象,节约系统资源,提供系统性能,同时降低了对象创建与垃圾回收的开销。

[3] 享元模式中的外部状态相对独立,使得对象可以在不同的环境中被复用(共享对象可以适应不同的外部环境),且不影响内部状态。

  (2)享元模式缺点

[1]提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。

[2]外部状态由客户端保存,共享对象读取外部状态的开销可能比较大。

[3]享元模式要求将内部状态与外部状态分离,这使得程序的逻辑复杂化,同时也增加了状态维护成本,使程序逻辑复杂。

[4]享元模式将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。

6.模式深入讲解

  (1)变与不变

    享元模式设计的重点就在于分离变与不变,把一个对象的状态分成内蕴状态和外蕴状态,内蕴状态是不变的,外蕴状态是可变的。然后通过共享不变的部分,达到减少对象数量、并节约内存的目的。在享元对象需要的时候,可以从外部传入外蕴状态给共享的对象,共享对象会在功能处理的时候,使用到自己内蕴状态和这些外蕴状态。

    事实上,分离变与不变是软件设计上最基本的方式之一,比如预留接口,为什么在这个地方要预留接口,一个常见的原因就是这里存在变化,可能在今后需要扩展、或者是改变已有的实现,因此预留接口做为“可插入性的保证”。

  (2)共享与不共享

  在享元模式中,享元对象又有共享与不共享之分,即单纯享元与复合享元,通常共享的是叶子对象,一般不共享的部分是由共享部分组合而成的,由于所有细粒度的叶子对象都已经缓存了,所以就不需要缓存树枝节点了,也就是单纯享元是需要共享的,复合享元不需要共享。

  (3)实例池

  在享元模式中,为了创建和管理共享的享元部分,引入了享元工厂,享元工厂中一般都包含有享元对象的实例池,享元对象就是缓存在这个实例池中的。

  简单介绍一点实例池的知识,所谓实例池,指的是缓存和管理对象实例的程序,通常实例池会提供对象实例的运行环境,并控制对象实例的生命周期。

  工业级的实例池实现上有两个最基本的难点,一个就是动态控制实例数量,一个就是动态分配实例来提供给外部使用。这些都是需要算法来做保证的。

  假如实例池里面已有了3个实例,但是客户端请求非常多,有些忙不过来,那么实例池的管理程序就应该判断出来,到底几个实例才能满足现在的客户需求,理想状况是刚刚好,就是既能够满足应用的需要,又不会造成对象实例的浪费,假如经过判断5个实例正好,那么实例池的管理程序就应该能动态的创建2个新的实例。

  这样运行了一段时间,客户端的请求减少了,这个时候实例池的管理程序又应该动态的判断,究竟几个实例是最好的,多了明显浪费资源,假如经过判断只需要1个实例就可以了,那么实例池的管理程序应该销毁掉多余的4个实例,以释放资源。这就是动态控制实例数量。

  对于动态分配实例,也说明一下吧,假如实例池里面有3个实例,这个时候来了一个新的请求,到底调度哪一个实例去执行客户的请求呢,如果有空闲实例,那就是它了,要是没有空闲实例呢,是新建一个实例,还是等待运行中的实例,等它运行完了就来处理这个请求呢?具体如何调度,也是需要算法来保障的。

  回到享元模式中来,享元工厂中的实例池可没有这么复杂,因为共享的享元对象基本上都是一个实例,一般不会出现同一个享元对象有多个实例的情况,这样就不用去考虑动态创建和销毁享元对象实例的功能;另外只有一个实例,也就不存在动态调度的麻烦,反正就是它了。

  这也主要是因为享元对象封装的多半是对象的内部状态,这些状态通常是不变的,有一个实例就够了,不需要动态控制生命周期,也不需要动态调度,它只需要做一个缓存而已,没有上升到真正的实例池那么个高度。

7.应用实例

  (1)Java中的String类型

    在JAVA语言中,String类型就是使用了享元模式。String对象是final类型,对象一旦创建就不可改变。在JAVA中字符串常量都是存在常量池中的,JAVA会确保一个字符串常量在常量池中只有一个拷贝。String a="abc",其中"abc"就是一个字符串常量。

public class Test {
    public static void main(String[] args) {        
        String a = "abc";
        String b = "abc";
        System.out.println(a==b);        
    }
}      

  上面的例子中结果为:true ,这就说明a和b两个引用都指向了常量池中的同一个字符串常量"abc"。这样的设计避免了在创建N多相同对象时所产生的不必要的大量的资源消耗。

继续阅读