java設計模式(11):享元模式
java 設計模式之享元模式
當前咱們國家正在大力倡導構建和諧社會,其中一個很重要的組成部分就是建設資源節約型社會,“浪費可恥,節儉光榮”。在軟體系統中,有時候也會存在資源浪費的情況,例如在計算機記憶體中儲存了多個完全相同或者非常相似的物件,如果這些物件的數量太多將導致系統執行代價過高,記憶體屬於計算機的“稀缺資源”,不應該用來“隨便浪費”,那麼是否存在一種技術可以用於節約記憶體使用空間,實現對這些相同或者相似物件的共享訪問呢?答案是肯定,這種技術就是我們本章將要學習的享元模式。
1.圍棋棋子的設計
Sunny軟體公司開發人員通過對圍棋軟體進行分析,發現在圍棋棋盤中包含大量的黑子和白子,它們的形狀、大小都一模一樣,只是出現的位置不同而已。如果將每一個棋子都作為一個獨立的物件儲存在記憶體中
2.享元模式概述
當一個軟體系統在執行時產生的物件數量太多,將導致執行代價過高,帶來系統性能下降等問題。
例如在一個文字字串中存在很多重複的字元,如果每一個字元都用一個單獨的物件來表示,將會佔用較多的記憶體空間,那麼我們如何去避免系統中出現大量相同或相似的物件,同時又不影響客戶端程式通過面向物件的方式對這些物件進行操作?享元模式正為解決這一類問題而誕生。
享元模式通過共享技術實現相同或相似物件的重用,在邏輯上每一個出現的字元都有一個物件與之對應,然而在物理上它們卻共享同一個享元物件,這個物件可以出現在一個字串的不同地方,相同的字元物件都指向同一個例項,在享元模式中,儲存這些共享例項物件的地方稱為享元池(Flyweight Pool)。我們可以針對每一個不同的字元建立一個享元物件,將其放在享元池中,需要時再從享元池取出。如下圖所示:
享元模式以共享的方式高效地支援大量細粒度物件的重用,享元物件能做到共享的關鍵是區分了內部狀態(Intrinsic State)和外部狀態(Extrinsic State)。下面將對享元的內部狀態和外部狀態進行簡單的介紹:
(1) 內部狀態是儲存在享元物件內部並且不會隨環境改變而改變的狀態,內部狀態可以共享。如字元的內容,不會隨外部環境的變化而變化,無論在任何環境下字元“a”始終是“a”,都不會變成“b”。
(2) 外部狀態是隨環境改變而改變的、不可以共享的狀態。享元物件的外部狀態通常由客戶端儲存,並在享元物件被建立之後,需要使用的時候再傳入到享元物件內部。一個外部狀態與另一個外部狀態之間是相互獨立的。如字元的顏色,可以在不同的地方有不同的顏色,例如有的“a”是紅色的,有的“a”是綠色的,字元的大小也是如此,有的“a”是五號字,有的“a”是四號字。而且字元的顏色和大小是兩個獨立的外部狀態,它們可以獨立變化,相互之間沒有影響,客戶端可以在使用時將外部狀態注入享元物件中。
正因為區分了內部狀態和外部狀態,我們可以將具有相同內部狀態的物件儲存在享元池中,享元池中的物件是可以實現共享的,需要的時候就將物件從享元池中取出,實現物件的複用。通過向取出的物件注入不同的外部狀態,可以得到一系列相似的物件,而這些物件在記憶體中實際上只儲存一份。
享元模式定義如下:
享元模式(Flyweight Pattern):運用共享技術有效地支援大量細粒度物件的複用。系統只使用少量的物件,而這些物件都很相似,狀態變化很小,可以實現物件的多次複用。由於享元模式要求能夠共享的物件必須是細粒度物件,因此它又稱為輕量級模式,它是一種物件結構型模式。 |
享元模式結構較為複雜,一般結合工廠模式一起使用,在它的結構圖中包含了一個享元工廠類,其結構圖如下圖所示:
在享元模式結構圖中包含如下幾個角色:
● 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 = new ConcreteFlyweight();
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) {
......
}
}
3.完整解決方案
為了節約儲存空間,提高系統性能,Sunny公司開發人員使用享元模式來設計圍棋軟體中的棋子,其基本結構如下圖所示:
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
棋子顏色:黑色
棋子顏色:黑色
棋子顏色:黑色
棋子顏色:白色
棋子顏色:白色
從輸出結果可以看出,雖然我們獲取了三個黑子物件和兩個白子物件,但是它們的記憶體地址相同,也就是說,它們實際上是同一個物件。在實現享元工廠類時我們使用了單例模式和簡單工廠模式,確保了享元工廠物件的唯一性,並提供工廠方法來向客戶端返回享元物件。
5.帶外部狀態的解決方案
Sunny軟體公司開發人員通過對圍棋棋子進行進一步分析,發現雖然黑色棋子和白色棋子可以共享,但是它們將顯示在棋盤的不同位置,如何讓相同的黑子或者白子能夠多次重複顯示且位於一個棋盤的不同地方?解決方法就是將棋子的位置定義為棋子的一個外部狀態,在需要時再進行設定。因此,我們在上圖中增加了一個新的類Coordinates(座標類),用於儲存每一個棋子的位置,修改之後的結構圖如下圖所示:
除了增加一個座標類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() 方法時,都設定了不同的外部狀態——座標值,因此相同的棋子物件雖然具有相同的顏色,但是它們的座標值不同,將顯示在棋盤的不同位置。
作者:liuwei