一、享元(Flyweight)模式
Flyweight在拳擊比賽中指最輕量級,即"蠅量級",有些作者翻譯為"羽量級"。這裡使用"享元模式"更能反映模式的用意。
享元模式以共享的方式高效地支持大量的細粒度對象。享元對象能做到共享的關鍵是區分內蘊狀態(Internal State)和外蘊狀態(External State)。內蘊狀態是存儲在享元對象內部並且不會隨環境改變而改變。因此內蘊狀態並可以共享。
外蘊狀態是隨環境改變而改變的、不可以共享的狀態。享元對象的外蘊狀態必須由客戶端保存,並在享元對象被創建之後,在需要使用的時候再傳入到享元對象內部。外蘊狀態與內蘊狀態是相互獨立的。
享元模式的應用
享元模式在編輯器系統中大量使用。一個文本編輯器往往會提供很多種字體,而通常的做法就是將每一個字母做成一個享元對象。享元對象的內蘊狀態就是這個字母,而字母在文本中的位置和字模風格等其他信息則是外蘊狀態。比如,字母a可能出現在文本的很多地方,雖然這些字母a的位置和字模風格不同,但是所有這些地方使用的都是同一個字母對象。這樣一來,字母對象就可以在整個系統中共享。
二、單純享元模式的結構
在單純享元模式中,所有的享元對象都是可以共享的。單純享元模式所涉及的角色如下:
抽象享元(Flyweight)角色:此角色是所有的具體享元類的超類,為這些類規定出需要實現的公共接口。那些需要外蘊狀態(External State)的操作可以通過調用商業方法以參數形式傳入。
具體享元(ConcreteFlyweight)角色:實現抽象享元角色所規定的接口。如果有內蘊狀態的話,必須負責為內蘊狀態提供存儲空間。享元對象的內蘊狀態必須與對象所處的周圍環境無關,從而使得享元對象可以在系統內共享的。
享元工廠(FlyweightFactory)角色:本角色負責創建和管理享元角色。本角色必須保證享元對象可以被系統適當地共享。當一個客戶端對象調用一個享元對象的時候,享元工廠角色會檢查系統中是否已經有一個復合要求的享元對象。如果已經有了,享元工廠角色就應當提供這個已有的享元對象;如果系統中沒有一個適當的享元對象的話,享元工廠角色就應當創建一個合適的享元對象。
客戶端(Client)角色:本角色需要維護一個對所有享元對象的引用。本角色需要自行存儲所有享元對象的外蘊狀態。
三、單純享元模式的示意性源代碼
// Flyweight pattern -- Structural example
using System;
using System.Collections;
// "FlyweightFactory"
class FlyweightFactory
{
// Fields
private Hashtable flyweights = new Hashtable();
// Constructors
public FlyweightFactory()
{
flyweights.Add("X", new ConcreteFlyweight());
flyweights.Add("Y", new ConcreteFlyweight());
flyweights.Add("Z", new ConcreteFlyweight());
}
// Methods
public Flyweight GetFlyweight(string key)
{
return((Flyweight)flyweights[ key ]);
}
}
// "Flyweight"
abstract class Flyweight
{
// Methods
abstract public void Operation( int extrinsicstate );
}
// "ConcreteFlyweight"
class ConcreteFlyweight : Flyweight
{
private string intrinsicstate = "A";
// Methods
override public void Operation( int extrinsicstate )
{
Console.WriteLine("ConcreteFlyweight: intrinsicstate {0}, extrinsicstate {1}",
intrinsicstate, extrinsicstate );
}
}
/**//// <summary>
/// Client test
/// </summary>
public class Client
{
public static void Main( string[] args )
{
// Arbitrary extrisic state
int extrinsicstate = 22;
FlyweightFactory f = new FlyweightFactory();
// Work with different flyweight instances
Flyweight fx = f.GetFlyweight("X");
fx.Operation( --extrinsicstate );
Flyweight fy = f.GetFlyweight("Y");
fy.Operation( --extrinsicstate );
Flyweight fz = f.GetFlyweight("Z");
fz.Operation( --extrinsicstate );
}
}
四、復合享元模式的結構
單純享元模式中,所有的享元對象都可以直接共享。下面考慮一個較為復雜的情況,即將一些單純享元使用合成模式加以復合,形成復合享元對象。這樣的復合享元對象本身不能共享,但是它們可以分解成單純享元對象,而後者則可以共享。
復合享元模式的類圖如下圖所示:
享元模式所涉及的角色有抽象享元角色、具體享元角色、復合享元角色、享員工廠角色,以及客戶端角色等。
抽象享元角色:此角色是所有的具體享元類的超類,為這些類規定出需要實現的公共接口。那些需要外蘊狀態(External State)的操作可以通過方法的參數傳入。抽象享元的接口使得享元變得可能,但是並不強制子類實行共享,因此並非所有的享元對象都是可以共享的。
具體享元(ConcreteFlyweight)角色:實現抽象享元角色所規定的接口。如果有內蘊狀態的話,必須負責為內蘊狀態提供存儲空間。享元對象的內蘊狀態必須與對象所處的周圍環境無關,從而使得享元對象可以在系統內共享。有時候具體享元角色又叫做單純具體享元角色,因為復合享元角色是由單純具體享元角色通過復合而成的。
復合享元(UnsharableFlyweight)角色:復合享元角色所代表的對象是不可以共享的,但是一個復合享元對象可以分解成為多個本身是單純享元對象的組合。復合享元角色又稱做不可共享的享元對象。
享元工廠(FlyweightFactoiy)角色:本角色負責創建和管理享元角色。本角色必須保證享元對象可以被系統適當地共享。當一個客戶端對象請求一個享元對象的時候,享元工廠角色需要檢查系統中是否已經有一個符合要求的享元對象,如果已經有了,享元工廠角色就應當提供這個已有的享元對象;如果系統中沒有一個適當的享元對象的話,享元工廠角色就應當創建一個新的合適的享元對象。
客戶端(Client)角色:本角色還需要自行存儲所有享元對象的外蘊狀態。
注:由於復合享元模式比較復雜,這裡就不再給出示意性代碼。通過將享元模式與合成模式組合在一起,可以確保復合享元中所包含的每個單純享元都具有相同的外蘊狀態,而這些單純享元的內蘊狀態往往不同。該部分內容可以參考《Java與模式》第31章內容。
五、一個咖啡攤的例子
在這個咖啡攤(Coffee Stall)所使用的系統裡,有一系列的咖啡"風味(Flavor)"。客人到攤位上購買咖啡,所有的咖啡均放在台子上,客人自己拿到咖啡後就離開攤位。咖啡有內蘊狀態,也就是咖啡的風味;咖啡沒有環境因素,也就是說沒有外蘊狀態。如果系統為每一杯咖啡都創建一個獨立的對象的話,那麼就需要創建出很多的細小對象來。這樣就不如把咖啡按照種類(即"風味")劃分,每一種風味的咖啡只創建一個對象,並實行共享。
使用咖啡攤主的語言來講,所有的咖啡都可按"風味"劃分成如Capucino、Espresso等,每一種風味的咖啡不論賣出多少杯,都是全同、不可分辨的。所謂共享,就是咖啡風味的共享,制造方法的共享等。因此,享元模式對咖啡攤來說,就意味著不需要為每一份單獨調制。攤主可以在需要時,一次性地調制出足夠一天出售的某一種風味的咖啡。
很顯然,這裡適合使用單純享元模式。系統的設計如下:
using System;
using System.Collections;
public abstract class Order
{
// 將咖啡賣給客人
public abstract void Serve();
// 返回咖啡的名字
public abstract string GetFlavor();
}
public class Flavor : Order
{
private string flavor;
// 構造函數,內蘊狀態以參數方式傳入
public Flavor(string flavor)
{
this.flavor = flavor;
}
// 返回咖啡的名字
public override string GetFlavor()
{
return this.flavor;
}
// 將咖啡賣給客人
public override void Serve()
{
Console.WriteLine("Serving flavor " + flavor);
}
}
public class FlavorFactory
{
private Hashtable flavors = new Hashtable();
public Order GetOrder(string key)
{
if(! flavors.ContainsKey(key))
flavors.Add(key, new Flavor(key));
return ((Order)flavors[key]);
}
public int GetTotalFlavorsMade()
{
return flavors.Count;
}
}
public class Client
{
private static FlavorFactory flavorFactory;
private static int ordersMade = 0;
public static void Main( string[] args )
{
flavorFactory = new FlavorFactory();
TakeOrder("Black Coffee");
TakeOrder("Capucino");
TakeOrder("Espresso");
TakeOrder("Capucino");
TakeOrder("Espresso");
TakeOrder("Black Coffee");
TakeOrder("Espresso");
TakeOrder("Espresso");
TakeOrder("Black Coffee");
TakeOrder("Capucino");
TakeOrder("Capucino");
TakeOrder("Black Coffee");
Console.WriteLine("\nTotal Orders made: " + ordersMade);
Console.WriteLine("\nTotal Flavor objects made: " +
flavorFactory.GetTotalFlavorsMade());
}
private static void TakeOrder(string aFlavor)
{
Order o = flavorFactory.GetOrder(aFlavor);
// 將咖啡賣給客人
o.Serve();
ordersMade++;
}
}
六、咖啡屋的例子
在前面的咖啡攤項目裡,由於沒有供客人坐的桌子,所有的咖啡均沒有環境的影響。換言之,咖啡僅有內蘊狀態,也就是咖啡的種類,而沒有外蘊狀態。
下面考慮一個規模稍稍大一點的咖啡屋(Coffee Shop)項目。屋子裡有很多的桌子供客人坐,系統除了需要提供咖啡的"風味"之外,還需要跟蹤咖啡被送到哪一個桌位上,因此,咖啡就有了桌子作為外蘊狀態。
由於外蘊狀態的存在,沒有外蘊狀態的單純享元模式不再符合要求。系統的設計可以利用有外蘊狀態的單純享元模式。系統的代碼如下:
using System;
using System.Collections;
public abstract class Order
{
// 將咖啡賣給客人
public abstract void Serve(Table table);
// 返回咖啡的名字
public abstract string GetFlavor();
}
public class Flavor : Order
{
private string flavor;
// 構造函數,內蘊狀態以參數方式傳入
public Flavor(string flavor)
{
this.flavor = flavor;
}
// 返回咖啡的名字
public override string GetFlavor()
{
return this.flavor;
}
// 將咖啡賣給客人
public override void Serve(Table table)
{
Console.WriteLine("Serving table {0} with flavor {1}", table.Number, flavor);
}
}
public class FlavorFactory
{
private Hashtable flavors = new Hashtable();
public Order GetOrder(string key)
{
if(! flavors.ContainsKey(key))
flavors.Add(key, new Flavor(key));
return ((Order)flavors[key]);
}
public int GetTotalFlavorsMade()
{
return flavors.Count;
}
}
public class Table
{
private int number;
public Table(int number)
{
this.number = number;
}
public int Number
{
get { return number; }
}
}
public class Client
{
private static FlavorFactory flavorFactory;
private static int ordersMade = 0;
public static void Main( string[] args )
{
flavorFactory = new FlavorFactory();
TakeOrder("Black Coffee");
TakeOrder("Capucino");
TakeOrder("Espresso");
TakeOrder("Capucino");
TakeOrder("Espresso");
TakeOrder("Black Coffee");
TakeOrder("Espresso");
TakeOrder("Espresso");
TakeOrder("Black Coffee");
TakeOrder("Capucino");
TakeOrder("Capucino");
TakeOrder("Black Coffee");
Console.WriteLine("\nTotal Orders made: " + ordersMade);
Console.WriteLine("\nTotal Flavor objects made: " +
flavorFactory.GetTotalFlavorsMade());
}
private static void TakeOrder(string aFlavor)
{
Order o = flavorFactory.GetOrder(aFlavor);
// 將咖啡賣給客人
o.Serve(new Table(++ordersMade));
}
}
七、享元模式應當在什麼情況下使用
當以下所有的條件都滿足時,可以考慮使用享元模式:
一個系統有大量的對象。
這些對象耗費大量的內存。
這些對象的狀態中的大部分都可以外部化。
這些對象可以按照內蘊狀態分成很多的組,當把外蘊對象從對象中剔除時,每一個組都可以僅用一個對象代替。
軟件系統不依賴於這些對象的身份,換言之,這些對象可以是不可分辨的。
滿足以上的這些條件的系統可以使用享元對象。
最後,使用享元模式需要維護一個記錄了系統已有的所有享元的表,而這需要耗費資源。因此,應當在有足夠多的享元實例可供共享時才值得使用享元模式。
八、享元模式的優點和缺點
享元模式的優點在於它大幅度地降低內存中對象的數量。但是,它做到這一點所付出的代價也是很高的:
享元模式使得系統更加復雜。為了使對象可以共享,需要將一些狀態外部化,這使得程序的邏輯復雜化。
享元模式將享元對象的狀態外部化,而讀取外部狀態使得運行時間稍微變長。