菜鳥的成長之路
在閻巨集博士的《JAVA與模式》一書中開頭是這樣描述享元(Flyweight)模式的:
Flyweight在拳擊比賽中指最輕量級,即“蠅量級”或“雨量級”,這裡選擇使用“享元模式”的意譯,是因為這樣更能反映模式的用意。享元模式是物件的結構模式。享元模式以共享的方式高效地支援大量的細粒度物件。
Java中的String型別
在JAVA語言中,String型別就是使用了享元模式。String物件是final型別,物件一旦建立就不可改變。在JAVA中字串常量都是存在常量池中的,JAVA會確保一個字串常量在常量池中只有一個拷貝。String a="abc",其中"abc"就是一個字串常量。
public class Test {
public static void main(String[] args) {
String a = "abc";
String b = "abc";
System.out.println(a==b);
}
}
上面的例子中結果為:true ,這就說明a和b兩個引用都指向了常量池中的同一個字串常量"abc"。這樣的設計避免了在建立N多相同物件時所產生的不必要的大量的資源消耗。
享元模式的結構
享元模式採用一個共享來避免大量擁有相同內容物件的開銷。這種開銷最常見、最直觀的就是記憶體的損耗。享元物件能做到共享的關鍵是區分內蘊狀態(Internal State)
一個內蘊狀態是儲存在享元物件內部的,並且是不會隨環境的改變而有所不同。因此,一個享元可以具有內蘊狀態並可以共享。
一個外蘊狀態是隨環境的改變而改變的、不可以共享的。享元物件的外蘊狀態必須由客戶端儲存,並在享元物件被建立之後,在需要使用的時候再傳入到享元物件內部。外蘊狀態不可以影響享元物件的內蘊狀態,它們是相互獨立的。
享元模式可以分成單純享元模式和複合享元模式兩種形式。
單純享元模式
在單純的享元模式中,所有的享元物件都是可以共享的。
單純享元模式所涉及到的角色如下:
● 抽象享元(Flyweight)角色 :
● 具體享元(ConcreteFlyweight)角色:實現抽象享元角色所規定出的介面。如果有內蘊狀態的話,必須負責為內蘊狀態提供儲存空間。
● 享元工廠(FlyweightFactory)角色 :本角色負責建立和管理享元角色。本角色必須保證享元物件可以被系統適當地共享。當一個客戶端物件呼叫一個享元物件的時候,享元工廠角色會檢查系統中是否已經有一個符合要求的享元物件。如果已經有了,享元工廠角色就應當提供這個已有的享元物件;如果系統中沒有一個適當的享元物件的話,享元工廠角色就應當建立一個合適的享元物件。
原始碼
抽象享元角色類
public interface Flyweight {
//一個示意性方法,引數state是外蘊狀態
public void operation(String state);
}
具體享元角色類ConcreteFlyweight有一個內蘊狀態,在本例中一個Character型別的intrinsicState屬性代表,它的值應當在享元物件被建立時賦予。所有的內蘊狀態在物件建立之後,就不會再改變了。
如果一個享元物件有外蘊狀態的話,所有的外部狀態都必須儲存在客戶端,在使用享元物件時,再由客戶端傳入享元物件。這裡只有一個外蘊狀態,operation()方法的引數state就是由外部傳入的外蘊狀態。
public class ConcreteFlyweight implements Flyweight {
private Character intrinsicState = null;
/**
* 建構函式,內蘊狀態作為引數傳入
* @param state
*/
public ConcreteFlyweight(Character state){
this.intrinsicState = state;
}
/**
* 外蘊狀態作為引數傳入方法中,改變方法的行為,
* 但是並不改變物件的內蘊狀態。
*/
@Override
public void operation(String state) {
// TODO Auto-generated method stub
System.out.println("Intrinsic State = " + this.intrinsicState);
System.out.println("Extrinsic State = " + state);
}
}
享元工廠角色類,必須指出的是,客戶端不可以直接將具體享元類例項化,而必須通過一個工廠物件,利用一個factory()方法得到享元物件。一般而言,享元工廠物件在整個系統中只有一個,因此也可以使用單例模式。
當客戶端需要單純享元物件的時候,需要呼叫享元工廠的factory()方法,並傳入所需的單純享元物件的內蘊狀態,由工廠方法產生所需要的享元物件。
public class FlyweightFactory {
private Map<Character,Flyweight> files = new HashMap<Character,Flyweight>();
public Flyweight factory(Character state){
//先從快取中查詢物件
Flyweight fly = files.get(state);
if(fly == null){
//如果物件不存在則建立一個新的Flyweight物件
fly = new ConcreteFlyweight(state);
//把這個新的Flyweight物件新增到快取中
files.put(state, fly);
}
return fly;
}
}
客戶端
public class Client {
public static void main(String[] args) {
// TODO Auto-generated method stub
FlyweightFactory factory = new FlyweightFactory();
Flyweight fly = factory.factory(new Character('a'));
fly.operation("First Call");
fly = factory.factory(new Character('b'));
fly.operation("Second Call");
fly = factory.factory(new Character('a'));
fly.operation("Third Call");
}
}
雖然客戶端申請了三個享元物件,但是實際建立的享元物件只有兩個,這就是共享的含義。執行結果如下:
複合享元模式
在單純享元模式中,所有的享元物件都是單純享元物件,也就是說都是可以直接共享的。還有一種較為複雜的情況,將一些單純享元使用合成模式加以複合,形成複合享元物件。這樣的複合享元物件本身不能共享,但是它們可以分解成單純享元物件,而後者則可以共享。
複合享元角色所涉及到的角色如下:
● 抽象享元(Flyweight)角色 :給出一個抽象介面,以規定出所有具體享元角色需要實現的方法。
● 具體享元(ConcreteFlyweight)角色:實現抽象享元角色所規定出的介面。如果有內蘊狀態的話,必須負責為內蘊狀態提供儲存空間。
● 複合享元(ConcreteCompositeFlyweight)角色 :複合享元角色所代表的物件是不可以共享的,但是一個複合享元物件可以分解成為多個本身是單純享元物件的組合。複合享元角色又稱作不可共享的享元物件。
● 享元工廠(FlyweightFactory)角色 :本角 色負責建立和管理享元角色。本角色必須保證享元物件可以被系統適當地共享。當一個客戶端物件呼叫一個享元物件的時候,享元工廠角色會檢查系統中是否已經有 一個符合要求的享元物件。如果已經有了,享元工廠角色就應當提供這個已有的享元物件;如果系統中沒有一個適當的享元物件的話,享元工廠角色就應當建立一個 合適的享元物件。
原始碼
抽象享元角色類
public interface Flyweight {
//一個示意性方法,引數state是外蘊狀態
public void operation(String state);
}
具體享元角色類
public class ConcreteFlyweight implements Flyweight {
private Character intrinsicState = null;
/**
* 建構函式,內蘊狀態作為引數傳入
* @param state
*/
public ConcreteFlyweight(Character state){
this.intrinsicState = state;
}
/**
* 外蘊狀態作為引數傳入方法中,改變方法的行為,
* 但是並不改變物件的內蘊狀態。
*/
@Override
public void operation(String state) {
// TODO Auto-generated method stub
System.out.println("Intrinsic State = " + this.intrinsicState);
System.out.println("Extrinsic State = " + state);
}
}
複合享元物件是由單純享元物件通過複合而成的,因此它提供了add()這樣的聚集管理方法。由於一個複合享元物件具有不同的聚集元素,這些聚集元素在複合享元物件被建立之後加入,這本身就意味著複合享元物件的狀態是會改變的,因此複合享元物件是不能共享的。
複合享元角色實現了抽象享元角色所規定的介面,也就是operation()方法,這個方法有一個引數,代表複合享元物件的外蘊狀態。一個複合享元物件的所有單純享元物件元素的外蘊狀態都是與複合享元物件的外蘊狀態相等的;而一個複合享元物件所含有的單純享元物件的內蘊狀態一般是不相等的,不然就沒有使用價值了。
public class ConcreteCompositeFlyweight implements Flyweight {
private Map<Character,Flyweight> files = new HashMap<Character,Flyweight>();
/**
* 增加一個新的單純享元物件到聚集中
*/
public void add(Character key , Flyweight fly){
files.put(key,fly);
}
/**
* 外蘊狀態作為引數傳入到方法中
*/
@Override
public void operation(String state) {
Flyweight fly = null;
for(Object o : files.keySet()){
fly = files.get(o);
fly.operation(state);
}
}
}
享元工廠角色提供兩種不同的方法,一種用於提供單純享元物件,另一種用於提供複合享元物件。
public class FlyweightFactory {
private Map<Character,Flyweight> files = new HashMap<Character,Flyweight>();
/**
* 複合享元工廠方法
*/
public Flyweight factory(List<Character> compositeState){
ConcreteCompositeFlyweight compositeFly = new ConcreteCompositeFlyweight();
for(Character state : compositeState){
compositeFly.add(state,this.factory(state));
}
return compositeFly;
}
/**
* 單純享元工廠方法
*/
public Flyweight factory(Character state){
//先從快取中查詢物件
Flyweight fly = files.get(state);
if(fly == null){
//如果物件不存在則建立一個新的Flyweight物件
fly = new ConcreteFlyweight(state);
//把這個新的Flyweight物件新增到快取中
files.put(state, fly);
}
return fly;
}
}
客戶端角色
public class Client {
public static void main(String[] args) {
List<Character> compositeState = new ArrayList<Character>();
compositeState.add('a');
compositeState.add('b');
compositeState.add('c');
compositeState.add('a');
compositeState.add('b');
FlyweightFactory flyFactory = new FlyweightFactory();
Flyweight compositeFly1 = flyFactory.factory(compositeState);
Flyweight compositeFly2 = flyFactory.factory(compositeState);
compositeFly1.operation("Composite Call");
System.out.println("---------------------------------");
System.out.println("複合享元模式是否可以共享物件:" + (compositeFly1 == compositeFly2));
Character state = 'a';
Flyweight fly1 = flyFactory.factory(state);
Flyweight fly2 = flyFactory.factory(state);
System.out.println("單純享元模式是否可以共享物件:" + (fly1 == fly2));
}
}
執行結果如下:
從執行結果可以看出,一個複合享元物件的所有單純享元物件元素的外蘊狀態都是與複合享元物件的外蘊狀態相等的。即外運狀態都等於Composite Call。
從執行結果可以看出,一個複合享元物件所含有的單純享元物件的內蘊狀態一般是不相等的。即內蘊狀態分別為b、c、a。
從執行結果可以看出,複合享元物件是不能共享的。即使用相同的物件compositeState通過工廠分別兩次創建出的物件不是同一個物件。
從執行結果可以看出,單純享元物件是可以共享的。即使用相同的物件state通過工廠分別兩次創建出的物件是同一個物件。
享元模式的優缺點
享元模式的優點在於它大幅度地降低記憶體中物件的數量。但是,它做到這一點所付出的代價也是很高的:
● 享元模式使得系統更加複雜。為了使物件可以共享,需要將一些狀態外部化,這使得程式的邏輯複雜化。
● 享元模式將享元物件的狀態外部化,而讀取外部狀態使得執行時間稍微變長