实现对象的复用——享元模式
本文轉載自 :http://blog.csdn.net/lovelion/article/details/7667781
當前咱們國家正在大力倡導構建和諧社會,其中一個很重要的組成部分就是建設資源節約型社會,“浪費可恥,節儉光榮”。在軟件系統中,有時候也會存在資源浪費的情況,例如在計算機內存中存儲了多個完全相同或者非常相似的對象,如果這些對象的數量太多將導致系統運行代價過高,內存屬于計算機的“稀缺資源”,不應該用來“隨便浪費”,那么是否存在一種技術可以用于節約內存使用空間,實現對這些相同或者相似對象的共享訪問呢?答案是肯定,這種技術就是我們本章將要學習的享元模式。
14.1 圍棋棋子的設計
| ????? Sunny軟件公司欲開發一個圍棋軟件,其界面效果如圖14-1所示:
圖14-1?圍棋軟件界面效果圖 |
????? Sunny軟件公司開發人員通過對圍棋軟件進行分析,發現在圍棋棋盤中包含大量的黑子和白子,它們的形狀、大小都一模一樣,只是出現的位置不同而已。如果將每一個棋子都作為一個獨立的對象存儲在內存中,將導致該圍棋軟件在運行時所需內存空間較大,如何降低運行代價、提高系統性能是Sunny公司開發人員需要解決的一個問題。為了解決這個問題,Sunny公司開發人員決定使用享元模式來設計該圍棋軟件的棋子對象,那么享元模式是如何實現節約內存進而提高系統性能的呢?別著急,下面讓我們正式進入享元模式的學習。
14.2 享元模式概述
????? 當一個軟件系統在運行時產生的對象數量太多,將導致運行代價過高,帶來系統性能下降等問題。例如在一個文本字符串中存在很多重復的字符,如果每一個字符都用一個單獨的對象來表示,將會占用較多的內存空間,那么我們如何去避免系統中出現大量相同或相似的對象,同時又不影響客戶端程序通過面向對象的方式對這些對象進行操作?享元模式正為解決這一類問題而誕生。享元模式通過共享技術實現相同或相似對象的重用,在邏輯上每一個出現的字符都有一個對象與之對應,然而在物理上它們卻共享同一個享元對象,這個對象可以出現在一個字符串的不同地方,相同的字符對象都指向同一個實例,在享元模式中,存儲這些共享實例對象的地方稱為享元池(Flyweight Pool)。我們可以針對每一個不同的字符創建一個享元對象,將其放在享元池中,需要時再從享元池取出。如圖14-2所示:
圖14-2?字符享元對象示意圖
????? 享元模式以共享的方式高效地支持大量細粒度對象的重用,享元對象能做到共享的關鍵是區分了內部狀態(Intrinsic State)和外部狀態(Extrinsic State)。下面將對享元的內部狀態和外部狀態進行簡單的介紹:
????? (1)??內部狀態是存儲在享元對象內部并且不會隨環境改變而改變的狀態,內部狀態可以共享。如字符的內容,不會隨外部環境的變化而變化,無論在任何環境下字符“a”始終是“a”,都不會變成“b”。
????? (2)??外部狀態是隨環境改變而改變的、不可以共享的狀態。享元對象的外部狀態通常由客戶端保存,并在享元對象被創建之后,需要使用的時候再傳入到享元對象內部。一個外部狀態與另一個外部狀態之間是相互獨立的。如字符的顏色,可以在不同的地方有不同的顏色,例如有的“a”是紅色的,有的“a”是綠色的,字符的大小也是如此,有的“a”是五號字,有的“a”是四號字。而且字符的顏色和大小是兩個獨立的外部狀態,它們可以獨立變化,相互之間沒有影響,客戶端可以在使用時將外部狀態注入享元對象中。
????? 正因為區分了內部狀態和外部狀態,我們可以將具有相同內部狀態的對象存儲在享元池中,享元池中的對象是可以實現共享的,需要的時候就將對象從享元池中取出,實現對象的復用。通過向取出的對象注入不同的外部狀態,可以得到一系列相似的對象,而這些對象在內存中實際上只存儲一份。
????? 享元模式定義如下:
| 享元模式(Flyweight Pattern):運用共享技術有效地支持大量細粒度對象的復用。系統只使用少量的對象,而這些對象都很相似,狀態變化很小,可以實現對象的多次復用。由于享元模式要求能夠共享的對象必須是細粒度對象,因此它又稱為輕量級模式,它是一種對象結構型模式。 |
享元模式結構較為復雜,一般結合工廠模式一起使用,在它的結構圖中包含了一個享元工廠類,其結構圖如圖14-3所示:
?
圖14-3?享元模式結構圖
????? 在享元模式結構圖中包含如下幾個角色:
????? ●?Flyweight(抽象享元類):通常是一個接口或抽象類,在抽象享元類中聲明了具體享元類公共的方法,這些方法可以向外界提供享元對象的內部數據(內部狀態),同時也可以通過這些方法來設置外部數據(外部狀態)。
????? ●?ConcreteFlyweight(具體享元類):它實現了抽象享元類,其實例稱為享元對象;在具體享元類中為內部狀態提供了存儲空間。通常我們可以結合單例模式來設計具體享元類,為每一個具體享元類提供唯一的享元對象。
????? ●?UnsharedConcreteFlyweight(非共享具體享元類):并不是所有的抽象享元類的子類都需要被共享,不能被共享的子類可設計為非共享具體享元類;當需要一個非共享具體享元類的對象時可以直接通過實例化創建。
????? ●?FlyweightFactory(享元工廠類):享元工廠類用于創建并管理享元對象,它針對抽象享元類編程,將各種類型的具體享元對象存儲在一個享元池中,享元池一般設計為一個存儲“鍵值對”的集合(也可以是其他類型的集合),可以結合工廠模式進行設計;當用戶請求一個具體享元對象時,享元工廠提供一個存儲在享元池中已創建的實例或者創建一個新的實例(如果不存在的話),返回新創建的實例并將其存儲在享元池中。
????? 在享元模式中引入了享元工廠類,享元工廠類的作用在于提供一個用于存儲享元對象的享元池,當用戶需要對象時,首先從享元池中獲取,如果享元池中不存在,則創建一個新的享元對象返回給用戶,并在享元池中保存該新增對象。典型的享元工廠類的代碼如下:
| class FlyweightFactory { ??? //定義一個HashMap用于存儲享元對象,實現享元池 ?????? private HashMap flyweights = newHashMap(); ?????? ?????? public Flyweight getFlyweight(String key){ ????????????? //如果對象存在,則直接從享元池獲取 ????????????? if(flyweights.containsKey(key)){ ???????????????????? return(Flyweight)flyweights.get(key); ????????????? } ????????????? //如果對象不存在,先創建一個新的對象添加到享元池中,然后返回 ????????????? else { ???????????????????? Flyweight fw = newConcreteFlyweight(); ???????????????????? flyweights.put(key,fw); ???????????????????? return fw; ????????????? } ?????? } } |
????? 享元類的設計是享元模式的要點之一,在享元類中要將內部狀態和外部狀態分開處理,通常將內部狀態作為享元類的成員變量,而外部狀態通過注入的方式添加到享元類中。典型的享元類代碼如下所示:
| class Flyweight { ?????//內部狀態intrinsicState作為成員變量,同一個享元對象其內部狀態是一致的 ?????? private String intrinsicState; ?????? ?????? public? Flyweight(String intrinsicState) { ????????????? this.intrinsicState=intrinsicState; ?????? } ?????? ??????? //外部狀態extrinsicState在使用時由外部設置,不保存在享元對象中,即使是同一個對象,在每一次調用時也可以傳入不同的外部狀態 ?????? public void operation(String? extrinsicState) { ????????????? ...... ?????? }????? } |
14.3 完整解決方案
????? ?為了節約存儲空間,提高系統性能,Sunny公司開發人員使用享元模式來設計圍棋軟件中的棋子,其基本結構如圖14-4所示:
圖14-4?圍棋棋子結構圖
?????? 在圖14-4中,IgoChessman充當抽象享元類,BlackIgoChessman和WhiteIgoChessman充當具體享元類,IgoChessmanFactory充當享元工廠類。完整代碼如下所示:
[java]?view plaincopy????? 編寫如下客戶端測試代碼:
[java]?view plaincopy?????? 編譯并運行程序,輸出結果如下:
| 判斷兩顆黑子是否相同:true 判斷兩顆白子是否相同:true 棋子顏色:黑色 棋子顏色:黑色 棋子顏色:黑色 棋子顏色:白色 棋子顏色:白色 |
?????? 從輸出結果可以看出,雖然我們獲取了三個黑子對象和兩個白子對象,但是它們的內存地址相同,也就是說,它們實際上是同一個對象。在實現享元工廠類時我們使用了單例模式和簡單工廠模式,確保了享元工廠對象的唯一性,并提供工廠方法來向客戶端返回享元對象。
14.5 帶外部狀態的解決方案
?????? Sunny軟件公司開發人員通過對圍棋棋子進行進一步分析,發現雖然黑色棋子和白色棋子可以共享,但是它們將顯示在棋盤的不同位置,如何讓相同的黑子或者白子能夠多次重復顯示且位于一個棋盤的不同地方?解決方法就是將棋子的位置定義為棋子的一個外部狀態,在需要時再進行設置。因此,我們在圖14-4中增加了一個新的類Coordinates(坐標類),用于存儲每一個棋子的位置,修改之后的結構圖如圖14-5所示:
圖14-5?引入外部狀態之后的圍棋棋子結構圖
?????? 在圖14-5中,除了增加一個坐標類Coordinates以外,抽象享元類IgoChessman中的display()方法也將對應增加一個Coordinates類型的參數,用于在顯示棋子時指定其坐標,Coordinates類和修改之后的IgoChessman類的代碼如下所示:
[java]?view plaincopy?????? 客戶端測試代碼修改如下:
[java]?view plaincopy?????? 編譯并運行程序,輸出結果如下:
| 判斷兩顆黑子是否相同:true 判斷兩顆白子是否相同:true 棋子顏色:黑色,棋子位置:1,2 棋子顏色:黑色,棋子位置:3,4 棋子顏色:黑色,棋子位置:1,3 棋子顏色:白色,棋子位置:2,5 棋子顏色:白色,棋子位置:2,4 |
?????? 從輸出結果可以看到,在每次調用display()方法時,都設置了不同的外部狀態——坐標值,因此相同的棋子對象雖然具有相同的顏色,但是它們的坐標值不同,將顯示在棋盤的不同位置。
14.5 單純享元模式和復合享元模式
?????? 標準的享元模式結構圖中既包含可以共享的具體享元類,也包含不可以共享的非共享具體享元類。但是在實際使用過程中,我們有時候會用到兩種特殊的享元模式:單純享元模式和復合享元模式,下面將對這兩種特殊的享元模式進行簡單的介紹:
?????? 1.單純享元模式
?????? 在單純享元模式中,所有的具體享元類都是可以共享的,不存在非共享具體享元類。單純享元模式的結構如圖14-6所示:
圖14-6??單純享元模式結構圖
?????? 2.復合享元模式
?????? 將一些單純享元對象使用組合模式加以組合,還可以形成復合享元對象,這樣的復合享元對象本身不能共享,但是它們可以分解成單純享元對象,而后者則可以共享。復合享元模式的結構如圖14-7所示:
圖14-7??復合享元模式結構圖
?????? 通過復合享元模式,可以確保復合享元類CompositeConcreteFlyweight中所包含的每個單純享元類ConcreteFlyweight都具有相同的外部狀態,而這些單純享元的內部狀態往往可以不同。如果希望為多個內部狀態不同的享元對象設置相同的外部狀態,可以考慮使用復合享元模式。
14.6 關于享元模式的幾點補充
?????? 1.與其他模式的聯用
?????? 享元模式通常需要和其他模式一起聯用,幾種常見的聯用方式如下:
?????? (1)在享元模式的享元工廠類中通常提供一個靜態的工廠方法用于返回享元對象,使用簡單工廠模式來生成享元對象。
?????? (2)在一個系統中,通常只有唯一一個享元工廠,因此可以使用單例模式進行享元工廠類的設計。
?????? (3)享元模式可以結合組合模式形成復合享元模式,統一對多個享元對象設置外部狀態。
?????? 2.享元模式與String類
?????? JDK類庫中的String類使用了享元模式,我們通過如下代碼來加以說明:
| class Demo { ?????? public? static void main(String args[]) { ????????????? String? str1 = "abcd"; ????????????? String? str2 = "abcd"; ????????????? String? str3 = "ab" + "cd"; ????????????? String? str4 = "ab"; ????????????? str4? += "cd"; ????????????? ????????????? System.out.println(str1? == str2); ????????????? System.out.println(str1? == str3); ????????????? System.out.println(str1? == str4); ????????????? ????????????? str2? += "e"; ????????????? System.out.println(str1? == str2); ?????? } } |
?????? 在Java語言中,如果每次執行類似String str1="abcd"的操作時都創建一個新的字符串對象將導致內存開銷很大,因此如果第一次創建了內容為"abcd"的字符串對象str1,下一次再創建內容相同的字符串對象str2時會將它的引用指向"abcd",不會重新分配內存空間,從而實現了"abcd"在內存中的共享。上述代碼輸出結果如下:
| true true false false |
?????? 可以看出,前兩個輸出語句均為true,說明str1、str2、str3在內存中引用了相同的對象;如果有一個字符串str4,其初值為"ab",再對它進行操作str4 += "cd",此時雖然str4的內容與str1相同,但是由于str4的初始值不同,在創建str4時重新分配了內存,所以第三個輸出語句結果為false;最后一個輸出語句結果也為false,說明當對str2進行修改時將創建一個新的對象,修改工作在新對象上完成,而原來引用的對象并沒有發生任何改變,str1仍然引用原有對象,而str2引用新對象,str1與str2引用了兩個完全不同的對象。
|
14.7 享元模式總結
?????? 當系統中存在大量相同或者相似的對象時,享元模式是一種較好的解決方案,它通過共享技術實現相同或相似的細粒度對象的復用,從而節約了內存空間,提高了系統性能。相比其他結構型設計模式,享元模式的使用頻率并不算太高,但是作為一種以“節約內存,提高性能”為出發點的設計模式,它在軟件開發中還是得到了一定程度的應用。
?????? 1.主要優點
?????? 享元模式的主要優點如下:
?????? (1)?可以極大減少內存中對象的數量,使得相同或相似對象在內存中只保存一份,從而可以節約系統資源,提高系統性能。
?????? (2)?享元模式的外部狀態相對獨立,而且不會影響其內部狀態,從而使得享元對象可以在不同的環境中被共享。
?????? 2.主要缺點
?????? 享元模式的主要缺點如下:
?????? (1)?享元模式使得系統變得復雜,需要分離出內部狀態和外部狀態,這使得程序的邏輯復雜化。
?????? (2)?為了使對象可以共享,享元模式需要將享元對象的部分狀態外部化,而讀取外部狀態將使得運行時間變長。
?????? 3.適用場景
?????? 在以下情況下可以考慮使用享元模式:
?????? (1)?一個系統有大量相同或者相似的對象,造成內存的大量耗費。
?????? (2)?對象的大部分狀態都可以外部化,可以將這些外部狀態傳入對象中。
?????? (3)?在使用享元模式時需要維護一個存儲享元對象的享元池,而這需要耗費一定的系統資源,因此,應當在需要多次重復使用享元對象時才值得使用享元模式。
|
總結
以上是生活随笔為你收集整理的实现对象的复用——享元模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 自定义语言的实现——解释器模式
- 下一篇: 复杂对象的组装与创建——建造者模式