設計模式:享元模式(Flyweight)
運用共享技術有效地支援大量細粒度的物件。又名“蠅量模式”。
在Java語言中,String型別就是使用了享元模式。String物件是final型別,物件一旦建立就不可改變。在JAVA中字串常量都是存在常量池中的,Java會確保一個字串常量在常量池中只有一個拷貝。譬如:
String a = "abc";
String b = "abc";
System.out.println(a==b);
輸出結果:true。這就說明了a和b量引用都指向了常量池中的同一個字串常量“abc”。這樣的設計避免了在建立N多相同物件時所產生的不必要的大量的資源消耗。
享元模式採用一個共享來避免大量擁有相同內容物件的開銷。這種開銷最常見、最直觀的就是記憶體的消耗。享元物件能做到共享的關鍵是區分內蘊狀態(Internal State)
一個內蘊狀態是儲存在享元物件內部的,並且是不會隨環境的改變而有所不同。因此,一個享元可以具有內蘊狀態並且可以共享。
一個外蘊狀態是隨環境的改變而改變的、不可以共享的。享元物件的外蘊狀態必須由客戶端儲存,並在享元物件被建立之後,在需要使用的時候再傳入到享元物件內部。外蘊狀態不可以影響享元物件的內蘊狀態,他們是相互獨立的。
享元模式可以分成單純享元模式和複合享元模式。
單純享元模式
包含的角色:
- 抽象享元角色(Flyweight):給出一個抽象介面,以規定出所有具體享元角色需要實現的方法。
- 具體享元角色(ConcreteFlyweight)
- 享元工廠角色(FlyweightFactory):本角色負責建立和管理享元角色。本角色必須保證享元物件可以被系統適當地共享。當一個客戶端物件呼叫一個享元物件的時候,享元工廠角色會檢查系統中是否已經有一個符合要求的享元物件。如果已經有了,享元工廠角色就應當提供這個已有的享元物件;如果系統中沒有一個適當的享元物件的話,享元工廠角色就應當建立一個合適的享元物件。
舉個簡單例子
1 抽象享元角色
public interface Flyweight
{
public void operation (String state);
}
2 具體享元角色
public class ConcreteFlyweight implements Flyweight
{
private String str;
public ConcreteFlyweight(String str)
{
this.str = str;
}
@Override
public void operation(String state)
{
System.out.println("內蘊狀態:"+str);
System.out.println("外蘊狀態:"+state);
}
}
3 享元工廠角色
public class FlyWeightFactory
{
private Map<String,ConcreteFlyweight> flyWeights = new HashMap<String, ConcreteFlyweight>();
public ConcreteFlyweight factory(String str)
{
ConcreteFlyweight flyweight = flyWeights.get(str);
if(null == flyweight)
{
flyweight = new ConcreteFlyweight(str);
flyWeights.put(str, flyweight);
}
return flyweight;
}
public int getFlyWeightSize()
{
return flyWeights.size();
}
}
4 測試程式碼
FlyWeightFactory factory = new FlyWeightFactory();
Flyweight f1 = factory.factory("a");
Flyweight f2 = factory.factory("b");
Flyweight f3 = factory.factory("a");
f1.operation("a fly weight");
f2.operation("b fly weight");
f3.operation("c fly weight");
System.out.println(f1 == f3);
System.out.println(factory.getFlyWeightSize());
輸出結果:
內蘊狀態:a
外蘊狀態:a fly weight
內蘊狀態:b
外蘊狀態:b fly weight
內蘊狀態:a
外蘊狀態:c fly weight
true
2
複合享元模式
在單純享元模式中,所有的享元物件都是單純享元物件,也就是說都是可以直接共享的,將一些單純享元使用合成模式加以複合,形成複合享元物件。這樣的複合享元物件本身不能共享,但是它們可以分解成單純享元物件,而後者則可以共享。
包含的角色
- 抽象享元角色(Flyweight):給出一個抽象介面,以規定出所有具體享元角色需要實現的方法。
- 具體享元角色(ConcreteFlyweight):實現抽象享元角色所規定出的介面。如果有內蘊狀態的話,必須負責為內蘊狀態提供儲存空間。
- 複合享元角色(ConcreteCompositeFlyweight):複合享元橘色所代表的物件是不可以共享的,但是一個複合享元物件可以分解成為多個本身是單純享元物件的組合。複合享元角色又稱作不可共享的享元物件。
- 享元工廠角色(FlyweightFactory):本角色負責建立和管理享元角色。本橘色必須保證享元物件可以被系統適當地共享。當一個客戶端物件呼叫一個享元物件的時候,享元工廠角色會檢查系統中是否已經有一個符合要求的享元物件。如果已經有了,享元工廠角色就應當提供這個已有的享元物件;如果系統中沒有一個適當的享元物件的話,享元工廠角色就應當建立一個合適的享元物件。
變更上面的例子:
1 抽象享元角色(同上)
2 具體享元角色(同上)
3 複合享元角色
複合享元物件是由單純享元物件通過複合而成的,因此提供了add()這樣的聚集管理方法。由於一個複合享元物件具有不同的聚集元素,這些聚集元素在複合享元物件被建立之後加入,這本身就意味著複合享元物件的狀態是會改變的,因此複合享元物件是不能共享的。
public class ConcreteCompositeFlyweight implements Flyweight
{
private Map<String,Flyweight> flyWeights = new HashMap<String, Flyweight>();
public void add(String key, Flyweight fly)
{
flyWeights.put(key, fly);
}
@Override
public void operation(String state)
{
Flyweight fly = null;
for(String s:flyWeights.keySet())
{
fly = flyWeights.get(s);
fly.operation(state);
}
}
}
4 享元工廠角色提供兩種不同的方法,一種用於提供單純享元物件,另一種用於提供複合享元物件。
public class FlyweightCompositeFactory
{
private Map<String,Flyweight> flyWeights = new HashMap<String, Flyweight>();
public Flyweight factory(List<String> compositeStates)
{
ConcreteCompositeFlyweight compositeFly = new ConcreteCompositeFlyweight();
for(String s: compositeStates)
{
compositeFly.add(s, this.factory(s));
}
return compositeFly;
}
public Flyweight factory(String s)
{
Flyweight fly = flyWeights.get(s);
if(fly == null)
{
fly = new ConcreteFlyweight(s);
flyWeights.put(s, fly);
}
return fly;
}
}
5 測試程式碼
List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
list.add("a");
list.add("b");
FlyweightCompositeFactory factory = new FlyweightCompositeFactory();
Flyweight f1 = factory.factory(list);
Flyweight f2 = factory.factory(list);
f1.operation("Composite Call");
System.out.println("=======");
System.out.println("複合享元模式是否可以共享物件:"+(f1 == f2));
String str = "a";
Flyweight f3 = factory.factory(str);
Flyweight f4 = factory.factory(str);
System.out.println("單純享元模式是否可以共享物件:"+(f3 == f4));
執行結果:
內蘊狀態:b
外蘊狀態:Composite Call
內蘊狀態:c
外蘊狀態:Composite Call
內蘊狀態:a
外蘊狀態:Composite Call
=======
複合享元模式是否可以共享物件:false
單純享元模式是否可以共享物件:true
由上例可知:一個符合享元物件的所有單純享元物件元素的外蘊狀態都是與複合享元物件的外蘊狀態相等的。一個複合享元物件所含有的單純享元物件的內蘊狀態一般是不相等的。複合享元物件是不能共享的。單純享元物件是可以共享的。
舉個更形象點的例子,比如去飯店吃飯,菜單隻有一份,而每個顧客點菜品卻各不相同,但是肯定會有重複,我們用上述的享元模式嘗試下模擬程式碼情形:
FlyweightCompositeFactory factory = new FlyweightCompositeFactory();
List<String> menuList = Arrays.asList("魚香肉絲","宮保雞丁","杭椒牛柳","平鍋魚","番茄炒蛋");
Flyweight f1 = factory.factory(menuList.subList(0, 2));
Flyweight f2 = factory.factory(menuList.subList(2, 3));
f1.operation("customer1的選單");
System.out.println("================");
f2.operation("customer2的選單");
程式碼輸出:
內蘊狀態:魚香肉絲
外蘊狀態:customer1的選單
內蘊狀態:宮保雞丁
外蘊狀態:customer1的選單
================
內蘊狀態:杭椒牛柳
外蘊狀態:customer2的選單
優缺點
優點:大幅度降低記憶體中物件的數量
缺點:享元模式使得系統更加複雜。為了使物件可以共享,需要將一些狀態外部化,這使得程式的邏輯複雜化。享元模式將享元物件的狀態外部化,而讀取外部狀態使得執行時間稍微變長。
Jdk中的享元模式
java.lang.Integer#valueOf(int)
java.lang.Boolean#valueOf(boolean)
java.lang.Byte#valueOf(byte)
java.lang.Character#valueOf(boolean)
歡迎支援筆者新書:《RabbitMQ實戰指南》以及關注微信公眾號:Kafka技術專欄。