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也有明顯區別,每個腳本文件執行開始,將會裝入全部須要的資源;執行結束後,又將佔用的資源就當即所有釋放,因此它基本上不會產生相似的性能問題,它的字符串處理的設計,天然也使用不到享元模式。