這裏就以去餐館吃飯爲例詳細的說明下享元模式的使用方式。去菜館點菜吃飯的過程大家一定都是輕車熟路了,這裏就不贅述。在例子中我使用了一個list來存放外蘊狀態和內蘊狀態的對應關係,而且提供了查詢每個客人點菜情況的方法。內蘊狀態在這裏代表了菜餚的種類,而外蘊狀態就是每盤菜餚的點菜人。
A 讓我們先來看看單純享元模式的實現吧。
先看下抽象享元角色的定義:GoF對享元模式的描述是:運用共享技術有效地支持大量細粒度的對象。
Flyweight模式是構造型模式之一,它通過與其他類似對象共享數據來減小內存佔用。也就是說在一個系統中如果有多個相同的對象,那麼只共享一份就可 以了,不必每個都去實例化一個對象。在Flyweight模式中,由於要產生各種各樣的對象,所以在Flyweight(享元)模式中常出現 Factory模式。Flyweight的內部狀態是用來共享的,Flyweight factory負責維護一個對象存儲池(Flyweight Pool)來存放內部狀態的對象。爲了調用方便,FlyweightFactory類一般使用Singleton模式實現。Flyweight模式是一個
提高程序效率和性能的模式,會大大加快程序的運行速度。
Flyweight模式的有效性很大程度上取決於如何使用它以及在何處使用它。當以下情況成立時使用Flyweight模式:
1 一個應用程序使用了大量的對象。
2 完全由於使用大量的對象,造成很大的存儲開銷。
3 對象的大多數狀態都可以變爲外部狀態。
4 如果刪除對象以外的狀態那麼可以用相對較少的共享對象取代很多組對象。
5 應用程序不依賴於對象標識。
其結構圖如下所示:
比如說,一個咖啡店有幾種口味的咖啡(拿鐵、摩卡、卡布奇諾等等),如果這家店接到分訂單要幾十杯咖啡。那麼顯然咖啡的口味就可以設置成共享的,而不必爲每一杯單獨生成。代碼實現如下:
import java.util.*;
public abstract class Order {
// 執行賣出動作
public abstract void sell();
}
public class FlavorOrder extends Order {
public String flavor;
// 獲取咖啡口味
public FlavorOrder(String flavor) {
this.flavor = flavor;
}
@Override
public void sell() {
// TODO Auto-generated method stub
System.out.println("賣出一份" + flavor + "的咖啡。");
}
}
public class FlavorFactory {
private Map<String, Order> flavorPool = new HashMap<String, Order>();
// 靜態工廠,負責生成訂單對象
private static FlavorFactory flavorFactory = new FlavorFactory();
private FlavorFactory() {
}
public static FlavorFactory getInstance() {
return flavorFactory;
}
public Order getOrder(String flavor) {
Order order = null;
if (flavorPool.containsKey(flavor)) {// 如果此映射包含指定鍵的映射關係,則返回 true
order = flavorPool.get(flavor);
} else {
order = new FlavorOrder(flavor);
flavorPool.put(flavor, order);
}
return order;
}
public int getTotalFlavorsMade() {
return flavorPool.size();
}
}
public class Client {
// 客戶下的訂單
private static List<Order> orders = new ArrayList<Order>();
// 訂單對象生成工廠
private static FlavorFactory flavorFactory;
// 增加訂單
private static void takeOrders(String flavor) {
orders.add(flavorFactory.getOrder(flavor));
}
public static void main(String[] args) {
// 訂單生成工廠
flavorFactory = FlavorFactory.getInstance();
// 增加訂單
takeOrders("摩卡");
takeOrders("卡布奇諾");
takeOrders("香草星冰樂");
takeOrders("香草星冰樂");
takeOrders("拿鐵");
takeOrders("卡布奇諾");
takeOrders("拿鐵");
takeOrders("卡布奇諾");
takeOrders("摩卡");
takeOrders("香草星冰樂");
takeOrders("卡布奇諾");
takeOrders("摩卡");
takeOrders("香草星冰樂");
takeOrders("拿鐵");
takeOrders("拿鐵");
// 賣咖啡
for (Order order : orders) {
order.sell();
}
// 打印生成的訂單java對象數量
System.out.println("\n客戶一共買了 " + orders.size() + " 杯咖啡! ");
// 打印生成的訂單java對象數量
System.out.println("共生成了 " + flavorFactory.getTotalFlavorsMade()
+ " 個 FlavorOrder java對象! ");
}
}
賣出一份摩卡的咖啡。
賣出一份卡布奇諾的咖啡。
賣出一份香草星冰樂的咖啡。
賣出一份香草星冰樂的咖啡。
賣出一份拿鐵的咖啡。
賣出一份卡布奇諾的咖啡。
賣出一份拿鐵的咖啡。
賣出一份卡布奇諾的咖啡。
賣出一份摩卡的咖啡。
賣出一份香草星冰樂的咖啡。
賣出一份卡布奇諾的咖啡。
賣出一份摩卡的咖啡。
賣出一份香草星冰樂的咖啡。
賣出一份拿鐵的咖啡。
賣出一份拿鐵的咖啡。
客戶一共買了 15 杯咖啡!
共生成了 4 個 FlavorOrder java對象!
正如輸入結果對比所示,把口味共享極大減少了對象數目,減小了內存消耗。
優缺點:
1)享元模式使得系統更加複雜。爲了使對象可以共享,需要將一些狀態外部化,這使得程序的邏輯複雜化。
2)享元模式將享元對象的狀態外部化,而讀取外部狀態使得運行時間稍微變長。
一、引子
讓我們先來複習下java中String類型的特性:String類型的對象一旦被創造就不可改變;當兩個String對象所包含的內容相同的時候,JVM只創建一個String對象對應這兩個不同的對象引用。讓我們來證實下着兩個特性吧(如果你已經瞭解,請跳過直接閱讀第二部分)。
先來驗證下第二個特性:
public class TestPattern {
public static void main(String[] args){
String n = "I Love Java";
String m = "I Love Java";
System.out.println(n==m);
}
}
這段代碼會告訴你n==m是true,這就說明了在JVM中n和m兩個引用了同一個String對象(如果你還分不清== 和 equals的區別的話,請先確認)。
那麼接着驗證下第一個特性:
在系統輸出之前加入一行代碼“m = m + "hehe";”,這時候n==m結果爲false,爲什麼剛纔兩個還是引用相同的對象,現在就不是了呢?原因就是在執行後添加語句時,m指向了一個新創建的String對象,而不是修改引用的對象。
呵呵,說着說着就差點跑了題,並不是每個String的特性都跟我們今天的主題有關的。
String類型的設計避免了在創建N多的String對象時產生的不必要的資源損耗,可以說是享元模式應用的範例,那麼讓我們帶着對享元的一點模糊的認識開始,來看看怎麼在自己的程序中正確的使用享元模式!
注:使用String類型請遵循《Effective Java》中的建議。
二、定義與分類
享元模式英文稱爲“Flyweight Pattern”,我非常感謝將Flyweight Pattern翻譯成享元模式的那位強人,因爲這個詞將這個模式使用的方式明白得表示了出來;如果翻譯成爲羽量級模式或者蠅量級模式等等,雖然可以含蓄的表現出使用此模式達到的目的,但是還是沒有抓住此模式的關鍵。
享元模式的定義爲:採用一個共享來避免大量擁有相同內容對象的開銷。這種開銷中最常見、直觀的就是內存的損耗。享元模式以共享的方式高效的支持大量的細粒度對象。
在名字和定義中都體現出了共享這一個核心概念,那麼怎麼來實現共享呢?要知道每個事物都是不同的,但是又有一定的共性,如果只有完全相同的事物才能共享,那麼享元模式可以說就是不可行的;因此我們應該儘量將事物的共性共享,而又保留它的個性。爲了做到這點,享元模式中區分了內蘊狀態和外蘊狀態。內蘊狀態就是共性,外蘊狀態就是個性了。
注:共享的對象必須是不可變的,不然一變則全變(如果有這種需求除外
內蘊狀態存 儲在享元內部,不會隨環境的改變而有所不同,是可以共享的;外蘊狀態是不可以共享的,它隨環境的改變而改變的,因此外蘊狀態是由客戶端來保持(因爲環境的 變化是由客戶端引起的)。在每個具體的環境下,客戶端將外蘊狀態傳遞給享元,從而創建不同的對象出來。至於怎樣來維護客戶端保持的外蘊狀態和享元內部保持 的內蘊狀態的對應關係,你先不用擔心這個問題,我們後面會涉及到的。
我們引用《Java與模式》中的分類,將享元模式分爲:單純享元模式和複合享元模式。在下一個小節裏面我們將詳細的講解這兩種享元模式。
三、結構
先從簡單的入手,看看單純享元模式的結構。
1) 抽象享元角色:爲具體享元角色規定了必須實現的方法,而外蘊狀態就是以參數的形式通過此方法傳入。在Java中可以由抽象類、接口來擔當。
2) 具體享元角色:實現抽象角色規定的方法。如果存在內蘊狀態,就負責爲內蘊狀態提供存儲空間。
3) 享元工廠角色:負責創建和管理享元角色。要想達到共享的目的,這個角色的實現是關鍵!
4) 客戶端角色:維護對所有享元對象的引用,而且還需要存儲對應的外蘊狀態。
來用類圖來形象地表示出它們的關係吧(對類圖的瞭解可以參看我關於類圖的blog)。
怎麼咋看咋像簡單工廠模式呢!沒錯,可以說結構型的單純享元模式和創建型的簡單工廠模式實現上非常相似,但是它的重點或者用意卻和工廠模式截然不同。工廠模式的使用主要是爲了使系統不依賴於實現得細節(見《深入淺出工廠模式》);而在享元模式的主要目的如前面所述:採用共享技術來避免大量擁有相同內容對象的開銷。正所謂“舊瓶裝新酒”阿!
再來看看複合享元模式的結構。
1) 抽象享元角色:爲具體享元角色規定了必須實現的方法,而外蘊狀態就是以參數的形式通過此方法傳入。在Java中可以由抽象類、接口來擔當。
2) 具體享元角色:實現抽象角色規定的方法。如果存在內蘊狀態,就負責爲內蘊狀態提供存儲空間。
3) 複合享元角色:它所代表的對象是不可以共享的,並且可以分解成爲多個單純享元對象的組合。
4) 享元工廠角色:負責創建和管理享元角色。要想達到共享的目的,這個角色的實現是關鍵!
5) 客戶端角色:維護對所有享元對象的引用,而且還需要存儲對應的外蘊狀態。
統比一下單純享元對象和複合享元對象,裏面只多出了一個複合享元角色,但是它的結構就發生了很大的變化。我們還是使用類圖來表示下:
你 也許又納悶了,這個也似曾相逢!單看左半部,和簡單工廠模式類似;再看右半部,怎麼這麼像合成模式呢(請參看關於合成模式的文章或者期待我的《深入淺出合 成模式》)!合成模式用在此處就是爲了將具體享元角色和複合享元角色同等對待和處理,通過將享元模式與合成模式組合在一起,可以確保複合享元中所包含的每 個單純享元都具有相同的外蘊狀態,而這些單純享元的內蘊狀態往往是不同的。
四、舉例
這裏就以去餐館吃飯爲例詳細的說明下享元模式的使用方式。去菜館點菜吃飯的過程大家一定都是輕車熟路了,這裏就不贅述。在例子中我使用了一個list來存放外蘊狀態和內蘊狀態的對應關係,而且提供了查詢每個客人點菜情況的方法。內蘊狀態在這裏代表了菜餚的種類,而外蘊狀態就是每盤菜餚的點菜人。
A 讓我們先來看看單純享元模式的實現吧。
先看下抽象享元角色的定義:
interface Menu
{
//規定了實現類必須實現設置內外關係的方法
public void setPersonMenu(String person , List list);
//規定了實現類必須實現查找外蘊狀態對應的內蘊狀態的方法
public List findPersonMenu(String person, List list);
}
這便是具體享元角色了:
class PersonMenu implements Menu
{
private String dish ;
//在構造方法中給內蘊狀態附值
public PersonMenu(String dish){
this.dish = dish ;
}
public synchronized void setPersonMenu(String person , List list)
{
list.add(person);
list.add(dish);
}
public List findPersonMenu(String person, List list)
{
List dishList = new ArrayList();
Iterator it = list.iterator();
while(it.hasNext())
{
if(person.equals((String)it.next()))
dishList.add(it.next());
}
return dishList ;
}
}
享元工廠角色,這可是關鍵所在,大家注意看!
class FlyweightFactory
{
private Map menuList = new HashMap();
private static FlyweightFactory factory = new FlyweightFactory();
//這裏還使用了單例模式,來使工廠對象只產生一個工廠實例
private FlyweightFactory(){}
public static FlyweightFactory getInstance()
{
return factory ;
}
//這就是享元模式同工廠模式的不同所在!!
public synchronized Menu factory(String dish)
{
//判斷如果內蘊狀態已經存在就不再重新生成,而是使用原來的,否則就重新生成
if(menuList.containsKey(dish))
{
return (Menu)menuList.get(dish);
}else{
Menu menu = new PersonMenu(dish);
menuList.put(dish,menu);
return menu;
}
}
//來驗證下是不是真的少產生了對象
public int getNumber()
{
return menuList.size();
}
}
我們使用客戶程序來試驗下吧。
class Client
{
private static FlyweightFactory factory ;
public static void main(String[] args)
{
List list1 = new ArrayList();
factory = FlyweightFactory.getInstance();
Menu list = factory.factory("尖椒土豆絲");
list.setPersonMenu("ai92",list1);
list = factory.factory("紅燒肉");
list.setPersonMenu("ai92",list1);
list = factory.factory("地三鮮");
list.setPersonMenu("ai92",list1);
list = factory.factory("地三鮮");
list.setPersonMenu("ai92",list1);
list = factory.factory("紅燜鯉魚");
list.setPersonMenu("ai92",list1);
list = factory.factory("紅燒肉");
list.setPersonMenu("ai921",list1);
list = factory.factory("紅燜鯉魚");
list.setPersonMenu("ai921",list1);
list = factory.factory("地三鮮");
list.setPersonMenu("ai921",list1);
System.out.println(factory.getNumber());
List list2 = list.findPersonMenu("ai921",list1);
Iterator it = list2.iterator();
while(it.hasNext())
{
System.out.println(" "+it.next());
}
}
}
這 樣便使用單純享元模式實現了這些功能,但是你是不是發現一個人點了好幾樣菜的時候是不是使用很不方便?而這種情況正好符合複合享元模式的使用條件:複合享 元中所包含的每個單純享元都具有相同的外蘊狀態,而這些單純享元的內蘊狀態往往是不同的。由於複合享元模式不能共享,所以不存在什麼內外狀態對應的問題。 所以在複合享元類中我們不用實現抽象享元對象中的方法,因此這裏採用的是透明式的合成模式。
那麼下面我就使用複合享元模式在上例的基礎上來實現一下。
首先要實現一個複合享元角色:
class PersonMenuMuch implements Menu
{
private Map MenuList = new HashMap();
public PersonMenuMuch(){}
//增加一個新的單純享元對象
public void add(String key , Menu menu)
{
MenuList.put(key , menu);
}
//兩個無爲的方法
public synchronized void setPersonMenu(String person , List list)
{ }
public List findPersonMenu(String person, List list)
{
List nothing = null ;
return nothing ;
}
}
在工廠方法中添加一個方法,實現重載。
public Menu factory(String[] dish)
{
PersonMenuMuch menu = new PersonMenuMuch();
String key = null ;
for(int i=0 ; i<dish.length ; i++)
{
key = dish[i];
menu.add(key , this.factory(key));//調用了單純享元角色的工廠方法
}
return menu ;
}
也許我的例子舉的不太恰當,但是基本上也能看出單純享元模式和複合享元模式在實現上的特點,如果這個目的達到了那就忘了這個糟糕的例子吧(不要讓它成了你深入理解享元模式的障礙),讓我們來分析下這兩種模式吧。
先從複雜度上來講,複合享元模式顯而易見是比單純享元模式複雜的。
再從享元模式的關鍵——共享,來分析:複合享元模式在共享上面是沒有達到預期的效果,可以說是沒有起到共享的目的。雖然對於它內部包含的單純享元角色來說還是能夠起到共享的作用,但是複合享元角色中一個內蘊狀態和對象使用了兩個Map來保存,這肯定是不會節省什麼空間和對象個數的。所以我認爲複合享元模式是違背享元模式初衷的。因此我們應該儘量使用單純享元模式。
在程序中你也許注意到,我對內蘊外蘊狀態對應關係的保持是採用一個list表來做的,這僅僅是個舉例,你完全可以採用各種能達到目的的方式來完成。這一點 也說明在享元模式中僅提供給我們怎麼來吧一個對象的狀態分開來達到共享,而對於關係的維護它是不關心的,也不是這個模式涉及的內容。
這樣我就把享元模式使用一個例子詳細的講解了一下。如果還是不太明白的話請回味下前面的定義與結構。只有兩者結合才能很好的體會到享元模式的用意。
五、使用優缺點
享元模式優點就在於它能夠大幅度的降低內存中對象的數量;而爲了做到這一步也帶來了它的缺點:它使得系統邏輯複雜化,而且在一定程度上外蘊狀態影響了系統的速度。
所以一定要切記使用享元模式的條件:
1) 系統中有大量的對象,他們使系統的效率降低。
2) 這些對象的狀態可以分離出所需要的內外兩部分。
外 蘊狀態和內蘊狀態的劃分以及兩者關係的對應也是非常值得重視的。只有將內外劃分妥當才能使內蘊狀態發揮它應有的作用;如果劃分失誤,在最糟糕的情況下系統 中的對象是一個也不會減少的!兩者的對應關係的維護和查找也是要花費一定的空間(當然這個比起不使用共享對象要小得多)和時間的,可以說享元模式就是使用 時間來換取空間的。在Gof的書中是使用了B樹來進行對應關係查找優化。
六、總結
也許你要長嘆一聲:這個享元模式未必太複雜了吧!這點是不得不承認的,也許由於它的複雜,實際應用也不是很多,這是我們更加無法看清他的真面目了。不過享 元模式並不是雞肋,它的精髓——共享是對我們系統優化非常有好處的,而且這種思想已經別越來越多的應用,這應該就算是享元模式的應用了吧。如果你已經領會 到了享元模式的精髓,那麼也就是掌握了享元模式了!
匆匆學完了享元模式,不知道理解上有沒有紕漏,希望大家能指正出來,一起共同進步!其實我一直想使用一個實際系統中或者實踐中的例子來講解享元模式,可是畢竟自己的工作經驗太少了!!於是想在網上找一些靈感來,可是狂搜一陣子也沒有發現什麼,於是就又落俗套的使用了一個比喻的例子。如果您對此深有體會的話,還煩請不吝賜教!!
{ String a = "abc";
String b = "abc";
System.out.println(a==b);
}
享元模式以共享的方式高效地支持大量的細粒度對象。
在面向對象的程序設計語言看來,一切事務都被描述成對象(Object)。對象擁有狀態(屬性)和行爲(方法),我們將具有相同行爲的對象抽象爲類(Class),類可以被看作只保留行爲的對象模板,類可以在運行時被重新賦予狀態數據從而形成了對象。
在運行時,對象佔用一定的內存空間用來存儲狀態數據。如果不作特殊的處理,儘管是由同一個類生成的兩個對象,而且這兩個對象的的狀態數據完 全相同,但在內存中還是會佔用兩份空間,這樣的情況對於程序的功能也許並沒有影響,但如果把狀態相同的同一類對象在內存中進行合併,必然會大大減少存儲空 間的浪費。
舉一個現實中的例子,某淘寶店經營一款暢銷女式皮鞋,每天需要處理大量的訂單信息,在訂單中需要註明客戶購買的皮鞋信息,我們將皮鞋產品抽象出來:
正如上面的代碼所描述,皮鞋分爲顏色、尺寸和庫存位置三項狀態數據。其中顏色和尺寸爲皮鞋的自然狀態,我們稱之爲對象內部狀態,這些狀態數據只與對象本身 有關,不隨外界環境的改變而發生變化。再來看庫存位置,我們將這個狀態稱爲對象的外部狀態,外部狀態與對象本身無必然關係,外部狀態總是因爲外界環境的改 變而變化,也就是說外部狀態是由外界環境來決定的。在本例中,皮鞋今天放在A倉庫,明天可能放在B倉庫,但無論存放在哪個倉庫,同一只皮鞋就是同一只皮 鞋,它的顏色和尺寸不會隨着存放位置的不同而發生變化。
享元模式的核心思想就是將內部狀態相同的對象在存儲時進行緩存。也就是說同一顏色同一尺寸的皮鞋,我們在內存中只保留一份實例,在訪問對象時,我們訪問的其實是對象緩存的版本,而不是每次都重新生成對象。
享元模式仍然允許對象具有外部屬性,由於我們訪問的始終是對象緩存的版本,所以我們在使用對象前,必須將外部狀態重新注入對象。由於享元模式禁止生成新的對象,所以在使用享元模式時,通常伴隨着工廠方法的應用。我們來看下面的例子:
通過ShoeFactory工廠,我們每次拿到的皮鞋都是緩存的版本,如果緩存中沒有我們需要的對象,則新創建對象然後加入緩存中。注意上例中對象的外部屬性position是如何注回對象的。
當我們在自己的業務場景中應用享元模式時,一定要注意分清對象的內部狀態和外部狀態,享元模式強調緩存的版本只能包含對象的內部狀態。
事實上,Java中的String和Integer類都是享元模式的應用的例子,String類內部對所有的字符串對象進行緩存,相同的字符串在內存中只會保留一個版本。類似的,Integer類在內部對小於255的整數也進行了緩存。
享元模式在企業級架構設計中應用的例子比比皆是,現代大型企業級應用中不可或缺的緩存體系也正是在享元模式的基礎上逐步完善和發展起來的。