天天看點

設計模式(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多相同對象時所産生的不必要的大量的資源消耗。

繼續閱讀