享元模式

1         享元模式的平常應用
面向對象的思想確實很好地解決了抽象性的問題,以致於在面向對象的眼中,萬事萬物一切皆對象。不可避免的是,採用面向對象的編程方式,可能會增長一些資源和性能上的開銷。不過,在大多數狀況下,這種影響還不是太大,因此,它帶來的空間和性能上的損耗相對於它的優勢而言,基本上不用考慮。可是,在某些特殊狀況下,大量細粒度對象的建立、銷燬以及存儲所形成的資源和性能上的損耗,可能會在系統運行時造成瓶頸。那麼咱們該如何去避免產生大量的細粒度對象,同時又不影響系統使用面向對象的方式進行操做呢?享元設計模式提供了一個比較好的解決方案。
公共交換電話網的使用方式就是生活中常見的享元模式的例子。公共交換電話網中的一些資源,例如撥號音發生器、振鈴發生器和撥號接收器,都是必須由全部用戶共享的,不可能爲每個人都配備一套這樣的資源,不然公共交換電話網的資源開銷也太大了。當一個用戶拿起聽筒打電話時,他根本不須要知道到底使用了多少資源,對用戶而言全部的事情就是有撥號音,撥打號碼,撥通電話就好了。因此,就有頗有人會共用一套資源,很是節省,這就是享元模式的基本思想。
假如咱們要開發一個相似MS Word的字處理軟件,下面分析一下將如何來實現。對於這樣一個字處理軟件,它須要處理的對象既有單個字符,又有由字符組成的段落以及整篇文檔,根據面向對象的設計思想,無論是字符、段落仍是文檔都應該做爲單個的對象去看待。咱們暫不考慮段落和文檔對象,只考慮單個的字符,因而能夠很容易的獲得下面的結構圖:
Java代碼:
//抽象的字符類
public abstract class Charactor{
    //屬性
    protected char letter;
    protected int fontsize;
    //顯示方法
    public abstract void display();
}
//具體的字符類A
public class CharactorA extends Charactor{
    //構造函數
    public CharactorA(){
        this.letter = 'A';
        this.fontsize = 12;
    }
    //顯示方法
    public void display(){
    try{
        System.out.println(this.letter);
    }catch(Exception err){
    }
    }
}
//具體的字符類B
public class CharactorB extends Charactor{
    //構造函數
    public CharactorB(){
        this.letter = 'B';
        this.fontsize = 12;
    }
    //顯示方法
    public void display(){
    try{
        System.out.println(this.letter);
    }catch(Exception err){
    }
    }
}
.Net代碼:
//抽象的字符類
public abstract class Charactor{
    //屬性
protected char letter;
protected int fontsize;
    //顯示方法
    public abstract void display();
}
//具體的字符類A
public class CharactorA : Charactor{ 
//構造函數
public CharactorA(){
      this.letter = 'A';
      this.fontsize = 12;    
}
//顯示方法
    public override void display(){
        Console.WriteLine(this.letter);
    }
}
//具體的字符類B
public class CharactorB: Charactor{ 
//構造函數
public CharactorB(){
      this.letter = 'B';
      this.fontsize= 14;    
}
//顯示方法
    public override void display(){
        Console.WriteLine(this.letter);
    }
}
咱們的這段代碼徹底符合面向對象的思想,可是卻爲此搭上了太多的性能損耗,代價很昂貴。
一篇文檔的字符數量極可能達到成千上萬,甚至更多,那麼在內存中就會同時存在大量的Charactor對象,這時候的內存開銷可想而知。
咱們對內存中的對象稍加分析就能發現,雖然內存中Character實例不少,可是裏面有不少實例差很少是相同的,好比CharactorA類的實例就有可能出現過不少次,這些不一樣的CharactorA的實例之間只有部分狀態不一樣而已。那麼,咱們是否是能夠只建立一份CharactorA的實例,而後讓整個系統共享這個實例呢?直接使用顯然是行不通的。好比一份文檔中使用了不少的字符A,雖然它們的屬性letter相同,都是'A',可是它們的fontsize卻不相同的,即字符大小並不相同。顯然,對於實例中的相同狀態是能夠共享的,不一樣的狀態就不能共享了。   
爲了解決這個問題,咱們能夠變換一下思路:首先將不可共享的狀態從類裏面剔除出去,即去掉fontsize這個屬性,這時候咱們再寫一下代碼:
Java代碼:
//抽象的字符類
public abstract class Charactor{
    //屬性
    protected char letter;
    //顯示方法
    public abstract void display();
}
//具體的字符類A
public class CharactorA extends Charactor{
    //構造函數
    public CharactorA(){
        this.letter = 'A';
    }
    //顯示方法
    public void display(){
    try{
        System.out.println(this.letter);
    }catch(Exception err){
    }
    }
}
//具體的字符類B
public class CharactorB extends Charactor{
    //構造函數
    public CharactorB(){
        this.letter = 'B';
    }
    //顯示方法
    public void display(){
    try{
        System.out.println(this.letter);
    }catch(Exception err){
    }
    }
}
.Net代碼:
//抽象的字符類
public abstract class Charactor{
    //屬性
    protected char letter;
    //顯示方法
    public abstract void display();
}
//具體的字符類A
public class CharactorA : Charactor{ 
    //構造函數
public CharactorA(){
      this.letter = 'A';
    }
//顯示方法
    public override void display(){
        Console.WriteLine(this.letter);
    }
}
//具體的字符類B
public class CharactorB: Charactor{ 
    //構造函數
public CharactorB(){
      this.letter = 'B';
    }
//顯示方法
    public override void display(){
        Console.WriteLine(this.letter);
    }
}
通過此次重構,類裏面剩餘的狀態就能夠共享了,下面咱們要作的工做就是要控制Charactor類的建立過程。若是已經存在了「A」字符這樣的實例,就不須要再建立,直接返回實例;若是沒有,則建立一個新的實例,這跟單例模式的作法有點相似了。在單例模式中是由類自身維護一個惟一的實例,享元模式則引入一個單獨的工廠類CharactorFactory來完成這項工做:
Java代碼:
public class CharactorFactory{
    private Hashtable<String,Charactor> charactors = new Hashtable<String,Charactor>();
    //構造函數
    public CharactorFactory(){
        charactors.put("A", new CharactorA());
        charactors.put("B", new CharactorB());
    }
    //得到指定字符實例
    public Charactor getCharactor(String key){
        Charactor charactor = (Charactor)charactors.get(key);
        if (charactor == null){
            if(key.equals("A")){
                charactor = new CharactorA();
            }else if(key.equals("B")){
                charactor = new CharactorB();
            }
            charactors.put(key, charactor);
        }
        return charactor;
    }
}
.Net代碼:
//享元類工廠
public class CharactorFactory{
    private Hashtable charactors = new Hashtable();
    //構造函數
    public CharactorFactory(){
        charactors.Add("A", new CharactorA());
        charactors.Add("B", new CharactorB());
    }
    //得到指定字符實例
    public Charactor getCharactor(String key){
        Charactor charactor = charactors[key] as Charactor;
        if (charactor == null){
            switch (key){
                case "A": charactor = new CharactorA(); break;
                case "B": charactor = new CharactorB(); break;
            }
            charactors.Add(key, charactor);
        }
        return charactor;
    }
}
通過本次重構,已經可使用同一個實例來存儲可共享的狀態,下面還須要作的工做就是要處理被剔除出去的那些不可共享的狀態。缺乏了這些不可共享的狀態,Charactor對象就沒法正常工做。
2         解決對象中不可共享狀態的問題
咱們先考慮一種比較簡單的解決方案:對於不能共享的狀態,不要在Charactor類中設置,而是由客戶程序在本身的代碼中進行設置:
Java代碼:
//客戶程序
public class ClinetTest{
public static void main(String[] args){
Charactor a = new CharactorA();
Charactor b = new CharactorB();
//顯示字符A
display(a,12);
        //顯示字符B
        display(b,14);
    }
 
    //設置字符的大小
public void display(Charactor objChar, int nSize){
    try{
        System.out.println("字符:" + objChar.letter + ",大小:" + nSize);
}catch(Exception err){
        }
}
}
.Net代碼:
//客戶程序
public class ClinetTest{
public static void Main(String[] args){
Charactor a = new CharactorA();
Charactor b = new CharactorB();
        //顯示字符A
        display(a,12);
        //顯示字符B
        display(b,14);
    }
 
    //設置字符的大小
    public void display(Charactor objChar, int nSize){
        Console.WriteLine("字符:" + objChar.letter + ",大小:" + nSize);
}
}
按照這樣的實現思路,能夠發現若是有多個客戶端程序使用的話,會出現大量的重複性的邏輯,就像上面這段代碼中的display方法同樣,須要全部的客戶端都提供,所以,這段代碼已經出現了臭味,很是不利於代碼的複用和維護。另外,把這些狀態和行爲移到客戶程序裏面破壞了面向對象中封裝的原則。
因此,咱們再次轉變咱們的實現思路,把這些不可共享的狀態仍然保留在Charactor對象中,把不一樣的狀態經過參數化的方式,由客戶程序注入。如下代碼是咱們最終實現的一個版本:
Java代碼:
//抽象的字符類
public abstract class Charactor{
    //屬性
    protected char letter;
    protected int fontsize;
    //顯示方法
public abstract void display();
//設置字體大小
public abstract void setFontSize(int fontsize);
}
//具體的字符類A
public class CharactorA extends Charactor{
    //構造函數
    public CharactorA(){
        this.letter = 'A';
        this.fontsize = 12;
    }
    //顯示方法
    public void display(){
    try{
        System.out.println(this.letter);
    }catch(Exception err){
    }
}
//設置字體大小
public void setFontSize(int fontsize){
    this.fontsize = fontsize;
}
}
//具體的字符類B
public class CharactorB extends Charactor{
    //構造函數
    public CharactorB(){
        this.letter = 'B';
        this.fontsize = 12;
    }
    //顯示方法
    public void display(){
    try{
        System.out.println(this.letter);
    }catch(Exception err){
    }
}
//設置字體大小
public void setFontSize(int fontsize){
    this.fontsize = fontsize;
}
}
//客戶程序
public class ClinetTest{
public static void main(String[] args){
Charactor a = new CharactorA();
Charactor b = new CharactorB();
//設置字符A的大小
a.setFontSize(12);
        //顯示字符B
        a.display();
//設置字符B的大小
b.setFontSize(14);
        //顯示字符B
b.display();
    }
}
 
.Net代碼:
//抽象的字符類
public abstract class Charactor{
    //屬性
protected char letter;
protected int fontsize;
    //顯示方法
public abstract void display();
//設置字體大小
public abstract void setFontSize(int fontsize);
}
//具體的字符類A
public class CharactorA : Charactor{ 
//構造函數
public CharactorA(){
      this.letter = 'A';
      this.fontsize = 12;    
}
//顯示方法
    public override void display(){
        Console.WriteLine(this.letter);
}
//設置字體大小
public override void setFontSize(int fontsize){
    this.fontsize = fontsize;
}
}
//具體的字符類B
public class CharactorB: Charactor{ 
//構造函數
public CharactorB(){
      this.letter = 'B';
      this.fontsize= 14;    
}
//顯示方法
    public override void display(){
        Console.WriteLine(this.letter);
}
//設置字體大小
public override void setFontSize(int fontsize){
    this.fontsize = fontsize;
}
}
//客戶程序
public class ClinetTest{
public static void Main(String[] args){
Charactor a = new CharactorA();
Charactor b = new CharactorB();
//設置字符A的大小
a.setFontSize(12);
        //顯示字符B
        a.display();
//設置字符B的大小
b.setFontSize(14);
        //顯示字符B
b.display();
    }
}
能夠看到這樣的實現明顯優於第一種實現思路,這就是享元模式的基本思想。咱們經過享元模式實現了節省存儲資源的目的。
3         什麼是享元模式
享元的英文是Flyweight,它是一個來自於體育方面的專業用語,在拳擊、摔跤和舉重比賽中特指最輕量的級別。把這個單詞移植到軟件工程裏面,也是用來表示特別小的對象,即細粒度對象。至於爲何咱們把Flyweight翻譯爲「享元」,能夠理解爲共享元對象,也就是共享細粒度對象。享元模式就是經過使用共享的方式,達到高效地支持大量的細粒度對象。它的目的就是節省佔用的空間資源,從而實現系統性能的改善。
咱們把享元對象的全部狀態分紅兩類,其實前面的例子中letter和fontsize屬性在運行時,就造成了兩類不一樣的狀態。
享元對象的第一類狀態稱爲內蘊狀態(Internal State)。它不會隨環境改變而改變,存儲在享元對象內部,所以內蘊狀態是能夠共享的,對於任何一個享元對象來說,它的值是徹底相同的。咱們例子中Character類的letter屬性,它表明的狀態就是內蘊狀態。
享元對象的第二類狀態稱爲外蘊狀態(External State)。它會隨環境的改變而改變,所以是不能夠共享的狀態,對於不一樣的享元對象來說,它的值多是不一樣的。享元對象的外蘊狀態必須由客戶端保存,在享元對象被建立以後,須要使用的時候再傳入到享元對象內部。咱們例子中Character類的fontsize屬性,它表明的狀態就是外蘊狀態。
因此享元的外蘊狀態與內蘊狀態是兩類相互獨立的狀態,彼此沒有關聯。
咱們按照前面的分析,給出享元模式的類圖:
 
 
享元模式類圖
 
l         抽象享元類(Flyweight)
它是全部具體享元類的超類。爲這些類規定出須要實現的公共接口,那些須要外蘊狀態(Exte的操做能夠經過方法的參數傳入。抽象享元的接口使得享元變得可能,可是並不強制子類實行共享,所以並不是全部的享元對象都是能夠共享的。
l         具體享元類(ConcreteFlyweight)
具體享元類實現了抽象享元類所規定的接口。若是有內蘊狀態的話,必須負責爲內蘊狀態提供存儲空間。享元對象的內蘊狀態必須與對象所處的周圍環境無關,從而使得享元對象能夠在系統內共享。有時候具體享元類又稱爲單純具體享元類,由於複合享元類是由單純具體享元角色經過複合而成的。
l         不能共享的具體享元類(UnsharableFlyweight)
不能共享的享元類,又叫作複合享元類。一個複合享元對象是由多個單享元對象組成,這些組成的對象是能夠共享的,可是複合享元類自己並不能共享。
l         享元工廠類(FlyweightFactoiy)
享元工廠類負責建立和管理享元對象。當一個客戶端對象請求一個享元對象的時候,享元工廠須要檢查系統中是否已經有一個符合要求的享元對象,若是已經有了,享元工廠角色就應當提供這個已有的享元對象;若是系統中沒有適當的享元對象的話,享元工廠角色就應當建立一個新的合適的享元對象。
l         客戶類(Client) 
客戶類須要自行存儲全部享元對象的外蘊狀態。
4         實現和使用享元模式須要注意的問題
面向對象雖然很好地解決了抽象性的問題,可是對於一個實際運行的軟件系統,咱們還須要考慮面向對象的代價問題,享元模式解決的就是面向對象的代價問題。享元模式採用對象共享的作法來下降系統中對象的個數,從而下降細粒度對象給系統帶來的內存壓力。
在具體實現方面,咱們要注意對象狀態的處理,必定要正確地區分對象的內蘊狀態和外蘊狀態,這是實現享元模式的關鍵所在。
享元模式的優勢在於它大幅度地下降內存中對象的數量。爲了作到這一點,享元模式也付出了必定的代價:
一、享元模式爲了使對象能夠共享,它須要將部分狀態外部化,這使得系統的邏輯變得複雜。
二、享元模式將享元對象的部分狀態外部化,而讀取外部狀態使得運行時間會有所加長。
另外,咱們還有一個比較關心的問題:到底系統須要知足什麼樣的條件才能使用享元模式。對於這個問題,咱們總結了如下幾條:
一、一個系統中存在着大量的細粒度對象;
二、這些細粒度對象耗費了大量的內存。
三、這些細粒度對象的狀態中的大部分均可之外部化;
四、這些細粒度對象能夠按照內蘊狀態分紅不少的組,當把外蘊對象從對象中剔除時,每個組均可以僅用一個對象代替。
五、軟件系統不依賴於這些對象的身份,換言之,這些對象能夠是不可分辨的。
知足以上的這些條件的系統可使用享元對象。最後,使用享元模式須要維護一個記錄了系統已有的全部享元的哈希表,也稱之爲對象池,而這也須要耗費必定的資源。所以,應當在有足夠多的享元實例可供共享時才值得使用享元模式。若是隻可以節省百八十個對象的話,仍是沒有必要引入享元模式的,畢竟性價比不高。
 
5         什麼狀況下使用享元模式
享元模式在通常的項目開發中並不經常使用,而是經常應用於系統底層的開發,以便解決系統的性能問題。
Java和.Net中的String類型就是使用了享元模式。若是在Java或者.NET中已經建立了一個字符串對象s1,那麼下次再建立相同的字符串s2的時候,系統只是把s2的引用指向s1所引用的具體對象,這就實現了相同字符串在內存中的共享。若是每次執行s1=「abc」操做的時候,都建立一個新的字符串對象的話,那麼內存的開銷會很大。
若是你們有興趣的話,能夠用下面的程序進行測試,就會知道s1和s2的引用是否一致: 
Java代碼:
String s1 = "測試字符串1";
String s2 = "測試字符串1";
//「==」用來判斷兩個對象是不是同一個,equals判斷字符串的值是否相等
if( s1 == s2 ){ 
System.out.println("二者一致");
}else{
System.out.println("二者不一致");
}
 
.Net代碼:
String s1 = "測試字符串1";
String s2 = "測試字符串1";
if( Object.ReferenceEquals(s1, s2) ){
Console.WriteLine("二者一致");
}else{
Console.WriteLine("二者不一致");
}
程序運行後,輸出的結果爲「二者一致」,這說明String類的設計採用了享元模式。若是s1的內容發生了變化,好比執行了s1 += "變化"的語句,那麼s1與s2的引用將再也不一致。
至於Php做爲一種弱類型語言,它的字符串類型是一種基本類型,不是對象。另外,它的執行方式與Java和.Net也有明顯區別,每個腳本文件執行開始,將會裝入全部須要的資源;執行結束後,又將佔用的資源就當即所有釋放,因此它基本上不會產生相似的性能問題,它的字符串處理的設計,天然也使用不到享元模式。
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息