java設計模式之享元模式
當前咱們國家正在大力倡導構建和諧社會,其中一個很重要的組成部分就是建設資源節約型社會,“浪費可恥,節儉光榮”。在軟件系統中,有時候也會存在資源浪費的情況,例如在計算機內存中存儲了多個完全相同或者非常相似的對象,如果這些對象的數量太多將導致系統運行代價過高,內存屬於計算機的“稀缺資源”,不應該用來“隨便浪費”,那麽是否存在一種技術可以用於節約內存使用空間,實現對這些相同或者相似對象的共享訪問呢?答案是肯定,這種技術就是我們本章將要學習的享元模式。
14.1 圍棋棋子的設計
Sunny軟件公司欲開發一個圍棋軟件,其界面效果如圖14-1所示: 圖14-1 圍棋軟件界面效果圖 |
Sunny軟件公司開發人員通過對圍棋軟件進行分析,發現在圍棋棋盤中包含大量的黑子和白子,它們的形狀、大小都一模一樣,只是出現的位置不同而已。如果將每一個棋子都作為一個獨立的對象存儲在內存中,將導致該圍棋軟件在運行時所需內存空間較大,如何降低運行代價、提高系統性能是Sunny公司開發人員需要解決的一個問題。為了解決這個問題,Sunny公司開發人員決定使用享元模式來設計該圍棋軟件的棋子對象,那麽享元模式是如何實現節約內存進而提高系統性能的呢?別著急,下面讓我們正式進入享元模式的學習。
14.2 享元模式概述
當一個軟件系統在運行時產生的對象數量太多,將導致運行代價過高,帶來系統性能下降等問題。例如在一個文本字符串中存在很多重復的字符,如果每一個字符都用一個單獨的對象來表示,將會占用較多的內存空間,那麽我們如何去避免系統中出現大量相同或相似的對象,同時又不影響客戶端程序通過面向對象的方式對這些對象進行操作?享元模式正為解決這一類問題而誕生。享元模式通過共享技術實現相同或相似對象的重用,在邏輯上每一個出現的字符都有一個對象與之對應,然而在物理上它們卻共享同一個享元對象
圖14-2 字符享元對象示意圖
享元模式以共享的方式高效地支持大量細粒度對象的重用,享元對象能做到共享的關鍵是區分了內部狀態(Intrinsic State)和外部狀態(Extrinsic State)。下面將對享元的內部狀態和外部狀態進行簡單的介紹:
(1) 內部狀態是存儲在享元對象內部並且不會隨環境改變而改變的狀態,內部狀態可以共享
(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充當享元工廠類。完整代碼如下所示:
import java.util.*; //圍棋棋子類:抽象享元類 abstract class IgoChessman { public abstract String getColor(); public void display() { System.out.println("棋子顏色:" + this.getColor()); } } //黑色棋子類:具體享元類 class BlackIgoChessman extends IgoChessman { public String getColor() { return "黑色"; } } //白色棋子類:具體享元類 class WhiteIgoChessman extends IgoChessman { public String getColor() { return "白色"; } } //圍棋棋子工廠類:享元工廠類,使用單例模式進行設計 class IgoChessmanFactory { private static IgoChessmanFactory instance = new IgoChessmanFactory(); private static Hashtable ht; //使用Hashtable來存儲享元對象,充當享元池 private IgoChessmanFactory() { ht = new Hashtable(); IgoChessman black,white; black = new BlackIgoChessman(); ht.put("b",black); white = new WhiteIgoChessman(); ht.put("w",white); } //返回享元工廠類的唯一實例 public static IgoChessmanFactory getInstance() { return instance; } //通過key來獲取存儲在Hashtable中的享元對象 public static IgoChessman getIgoChessman(String color) { return (IgoChessman)ht.get(color); } }
編寫如下客戶端測試代碼:
class Client { public static void main(String args[]) { IgoChessman black1,black2,black3,white1,white2; IgoChessmanFactory factory; //獲取享元工廠對象 factory = IgoChessmanFactory.getInstance(); //通過享元工廠獲取三顆黑子 black1 = factory.getIgoChessman("b"); black2 = factory.getIgoChessman("b"); black3 = factory.getIgoChessman("b"); System.out.println("判斷兩顆黑子是否相同:" + (black1==black2)); //通過享元工廠獲取兩顆白子 white1 = factory.getIgoChessman("w"); white2 = factory.getIgoChessman("w"); System.out.println("判斷兩顆白子是否相同:" + (white1==white2)); //顯示棋子 black1.display(); black2.display(); black3.display(); white1.display(); white2.display(); } }
編譯並運行程序,輸出結果如下:
判斷兩顆黑子是否相同:true 判斷兩顆白子是否相同:true 棋子顏色:黑色 棋子顏色:黑色 棋子顏色:黑色 棋子顏色:白色 棋子顏色:白色 |
從輸出結果可以看出,雖然我們獲取了三個黑子對象和兩個白子對象,但是它們的內存地址相同,也就是說,它們實際上是同一個對象。在實現享元工廠類時我們使用了單例模式和簡單工廠模式,確保了享元工廠對象的唯一性,並提供工廠方法來向客戶端返回享元對象。
14.5 帶外部狀態的解決方案
Sunny軟件公司開發人員通過對圍棋棋子進行進一步分析,發現雖然黑色棋子和白色棋子可以共享,但是它們將顯示在棋盤的不同位置,如何讓相同的黑子或者白子能夠多次重復顯示且位於一個棋盤的不同地方?解決方法就是將棋子的位置定義為棋子的一個外部狀態,在需要時再進行設置。因此,我們在圖14-4中增加了一個新的類Coordinates(坐標類),用於存儲每一個棋子的位置,修改之後的結構圖如圖14-5所示:
圖14-5 引入外部狀態之後的圍棋棋子結構圖
在圖14-5中,除了增加一個坐標類Coordinates以外,抽象享元類IgoChessman中的display()方法也將對應增加一個Coordinates類型的參數,用於在顯示棋子時指定其坐標,Coordinates類和修改之後的IgoChessman類的代碼如下所示:
class Coordinates { private int x; private int y; public Coordinates(int x,int y) { this.x = x; this.y = y; } public int getX() { return this.x; } public void setX(int x) { this.x = x; } public int getY() { return this.y; } public void setY(int y) { this.y = y; } } //圍棋棋子類:抽象享元類 abstract class IgoChessman { public abstract String getColor(); public void display(Coordinates coord){ System.out.println("棋子顏色:" + this.getColor() + ",棋子位置:" + coord.getX() + "," + coord.getY() ); } }
客戶端測試代碼修改如下:
class Client { public static void main(String args[]) { IgoChessman black1,black2,black3,white1,white2; IgoChessmanFactory factory; //獲取享元工廠對象 factory = IgoChessmanFactory.getInstance(); //通過享元工廠獲取三顆黑子 black1 = factory.getIgoChessman("b"); black2 = factory.getIgoChessman("b"); black3 = factory.getIgoChessman("b"); System.out.println("判斷兩顆黑子是否相同:" + (black1==black2)); //通過享元工廠獲取兩顆白子 white1 = factory.getIgoChessman("w"); white2 = factory.getIgoChessman("w"); System.out.println("判斷兩顆白子是否相同:" + (white1==white2)); //顯示棋子,同時設置棋子的坐標位置 black1.display(new Coordinates(1,2)); black2.display(new Coordinates(3,4)); black3.display(new Coordinates(1,3)); white1.display(new Coordinates(2,5)); white2.display(new Coordinates(2,4)); } }
編譯並運行程序,輸出結果如下:
判斷兩顆黑子是否相同:true 判斷兩顆白子是否相同:true 棋子顏色:黑色,棋子位置:1,2 棋子顏色:黑色,棋子位置:3,4 棋子顏色:黑色,棋子位置:1,3 棋子顏色:白色,棋子位置:2,5 棋子顏色:白色,棋子位置:2,4 |
從輸出結果可以看到,在每次調用display()方法時,都設置了不同的外部狀態——坐標值,因此相同的棋子對象雖然具有相同的顏色,但是它們的坐標值不同,將顯示在棋盤的不同位置。
【作者:劉偉 http://blog.csdn.net/lovelion】
java設計模式之享元模式