一、 享元(Flyweight)模式
Flyweight在拳擊比賽中指最輕量級,即"蠅量級",有些作者翻譯為"羽量級"。這里使用"享元模式"更能反映模式的用意。
享元模式以共享的方式高效地支持大量的細粒度對象。享元對象能做到共享的關(guān)鍵是區分內蘊狀態(tài)(Internal State)和外蘊狀態(tài)(External State)。內蘊狀態(tài)是存儲在享元對象內部并且不會(huì )隨環(huán)境改變而改變。因此內蘊狀態(tài)并可以共享。
外蘊狀態(tài)是隨環(huán)境改變而改變的、不可以共享的狀態(tài)。享元對象的外蘊狀態(tài)必須由客戶(hù)端保存,并在享元對象被創(chuàng )建之后,在需要使用的時(shí)候再傳入到享元對象內部。外蘊狀態(tài)與內蘊狀態(tài)是相互獨立的。
享元模式的應用
享元模式在編輯器系統中大量使用。一個(gè)文本編輯器往往會(huì )提供很多種字體,而通常的做法就是將每一個(gè)字母做成一個(gè)享元對象。享元對象的內蘊狀態(tài)就是這個(gè)字母,而字母在文本中的位置和字模風(fēng)格等其他信息則是外蘊狀態(tài)。比如,字母a可能出現在文本的很多地方,雖然這些字母a的位置和字模風(fēng)格不同,但是所有這些地方使用的都是同一個(gè)字母對象。這樣一來(lái),字母對象就可以在整個(gè)系統中共享。
二、 單純享元模式的結構
在單純享元模式中,所有的享元對象都是可以共享的。單純享元模式所涉及的角色如下:
抽象享元(Flyweight)角色:此角色是所有的具體享元類(lèi)的超類(lèi),為這些類(lèi)規定出需要實(shí)現的公共接口。那些需要外蘊狀態(tài)(External State)的操作可以通過(guò)調用商業(yè)方法以參數形式傳入。
具體享元(ConcreteFlyweight)角色:實(shí)現抽象享元角色所規定的接口。如果有內蘊狀態(tài)的話(huà),必須負責為內蘊狀態(tài)提供存儲空間。享元對象的內蘊狀態(tài)必須與對象所處的周?chē)h(huán)境無(wú)關(guān),從而使得享元對象可以在系統內共享的。
享元工廠(chǎng)(FlyweightFactory)角色:本角色負責創(chuàng )建和管理享元角色。本角色必須保證享元對象可以被系統適當地共享。當一個(gè)客戶(hù)端對象調用一個(gè)享元對象的時(shí)候,享元工廠(chǎng)角色會(huì )檢查系統中是否已經(jīng)有一個(gè)復合要求的享元對象。如果已經(jīng)有了,享元工廠(chǎng)角色就應當提供這個(gè)已有的享元對象;如果系統中沒(méi)有一個(gè)適當的享元對象的話(huà),享元工廠(chǎng)角色就應當創(chuàng )建一個(gè)合適的享元對象。
客戶(hù)端(Client)角色:本角色需要維護一個(gè)對所有享元對象的引用。本角色需要自行存儲所有享元對象的外蘊狀態(tài)。
三、 單純享元模式的示意性源代碼
// 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 );
}
}
四、 復合享元模式的結構
單純享元模式中,所有的享元對象都可以直接共享。下面考慮一個(gè)較為復雜的情況,即將一些單純享元使用合成模式加以復合,形成復合享元對象。這樣的復合享元對象本身不能共享,但是它們可以分解成單純享元對象,而后者則可以共享。
復合享元模式的類(lèi)圖如下圖所示:
享元模式所涉及的角色有抽象享元角色、具體享元角色、復合享元角色、享員工廠(chǎng)角色,以及客戶(hù)端角色等。
抽象享元角色:此角色是所有的具體享元類(lèi)的超類(lèi),為這些類(lèi)規定出需要實(shí)現的公共接口。那些需要外蘊狀態(tài)(External State)的操作可以通過(guò)方法的參數傳入。抽象享元的接口使得享元變得可能,但是并不強制子類(lèi)實(shí)行共享,因此并非所有的享元對象都是可以共享的。
具體享元(ConcreteFlyweight)角色:實(shí)現抽象享元角色所規定的接口。如果有內蘊狀態(tài)的話(huà),必須負責為內蘊狀態(tài)提供存儲空間。享元對象的內蘊狀態(tài)必須與對象所處的周?chē)h(huán)境無(wú)關(guān),從而使得享元對象可以在系統內共享。有時(shí)候具體享元角色又叫做單純具體享元角色,因為復合享元角色是由單純具體享元角色通過(guò)復合而成的。
復合享元(UnsharableFlyweight)角色:復合享元角色所代表的對象是不可以共享的,但是一個(gè)復合享元對象可以分解成為多個(gè)本身是單純享元對象的組合。復合享元角色又稱(chēng)做不可共享的享元對象。
享元工廠(chǎng)(FlyweightFactoiy)角色:本角色負責創(chuàng )建和管理享元角色。本角色必須保證享元對象可以被系統適當地共享。當一個(gè)客戶(hù)端對象請求一個(gè)享元對象的時(shí)候,享元工廠(chǎng)角色需要檢查系統中是否已經(jīng)有一個(gè)符合要求的享元對象,如果已經(jīng)有了,享元工廠(chǎng)角色就應當提供這個(gè)已有的享元對象;如果系統中沒(méi)有一個(gè)適當的享元對象的話(huà),享元工廠(chǎng)角色就應當創(chuàng )建一個(gè)新的合適的享元對象。
客戶(hù)端(Client)角色:本角色還需要自行存儲所有享元對象的外蘊狀態(tài)。
注:由于復合享元模式比較復雜,這里就不再給出示意性代碼。通過(guò)將享元模式與合成模式組合在一起,可以確保復合享元中所包含的每個(gè)單純享元都具有相同的外蘊狀態(tài),而這些單純享元的內蘊狀態(tài)往往不同。該部分內容可以參考《Java與模式》第31章內容。
五、 一個(gè)咖啡攤的例子
在這個(gè)咖啡攤(CoffeeStall)所使用的系統里,有一系列的咖啡"風(fēng)味(Flavor)"??腿说綌偽簧腺徺I(mǎi)咖啡,所有的咖啡均放在臺子上,客人自己拿到咖啡后就離開(kāi)攤位??Х扔袃忍N狀態(tài),也就是咖啡的風(fēng)味;咖啡沒(méi)有環(huán)境因素,也就是說(shuō)沒(méi)有外蘊狀態(tài)。如果系統為每一杯咖啡都創(chuàng )建一個(gè)獨立的對象的話(huà),那么就需要創(chuàng )建出很多的細小對象來(lái)。這樣就不如把咖啡按照種類(lèi)(即"風(fēng)味")劃分,每一種風(fēng)味的咖啡只創(chuàng )建一個(gè)對象,并實(shí)行共享。
使用咖啡攤主的語(yǔ)言來(lái)講,所有的咖啡都可按"風(fēng)味"劃分成如Capucino、Espresso等,每一種風(fēng)味的咖啡不論賣(mài)出多少杯,都是全同、不可分辨的。所謂共享,就是咖啡風(fēng)味的共享,制造方法的共享等。因此,享元模式對咖啡攤來(lái)說(shuō),就意味著(zhù)不需要為每一份單獨調制。攤主可以在需要時(shí),一次性地調制出足夠一天出售的某一種風(fēng)味的咖啡。
很顯然,這里適合使用單純享元模式。系統的設計如下:
using System;
using System.Collections;
public abstract class Order
{
// 將咖啡賣(mài)給客人
public abstract void Serve();
// 返回咖啡的名字
public abstract string GetFlavor();
}
public class Flavor : Order
{
private string flavor;
// 構造函數,內蘊狀態(tài)以參數方式傳入
public Flavor(string flavor)
{
this.flavor = flavor;
}
// 返回咖啡的名字
public override string GetFlavor()
{
return this.flavor;
}
// 將咖啡賣(mài)給客人
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);
// 將咖啡賣(mài)給客人
o.Serve();
ordersMade++;
}
}
六、 咖啡屋的例子
在前面的咖啡攤項目里,由于沒(méi)有供客人坐的桌子,所有的咖啡均沒(méi)有環(huán)境的影響。換言之,咖啡僅有內蘊狀態(tài),也就是咖啡的種類(lèi),而沒(méi)有外蘊狀態(tài)。
下面考慮一個(gè)規模稍稍大一點(diǎn)的咖啡屋(Coffee Shop)項目。屋子里有很多的桌子供客人坐,系統除了需要提供咖啡的"風(fēng)味"之外,還需要跟蹤咖啡被送到哪一個(gè)桌位上,因此,咖啡就有了桌子作為外蘊狀態(tài)。
由于外蘊狀態(tài)的存在,沒(méi)有外蘊狀態(tài)的單純享元模式不再符合要求。系統的設計可以利用有外蘊狀態(tài)的單純享元模式。系統的代碼如下:
using System;
using System.Collections;
public abstract class Order
{
// 將咖啡賣(mài)給客人
public abstract void Serve(Table table);
// 返回咖啡的名字
public abstract string GetFlavor();
}
public class Flavor : Order
{
private string flavor;
// 構造函數,內蘊狀態(tài)以參數方式傳入
public Flavor(string flavor)
{
this.flavor = flavor;
}
// 返回咖啡的名字
public override string GetFlavor()
{
return this.flavor;
}
// 將咖啡賣(mài)給客人
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);
// 將咖啡賣(mài)給客人
o.Serve(new Table(++ordersMade));
}
}
七、 享元模式應當在什么情況下使用
當以下所有的條件都滿(mǎn)足時(shí),可以考慮使用享元模式:
- 一個(gè)系統有大量的對象。
- 這些對象耗費大量的內存。
- 這些對象的狀態(tài)中的大部分都可以外部化。
- 這些對象可以按照內蘊狀態(tài)分成很多的組,當把外蘊對象從對象中剔除時(shí),每一個(gè)組都可以?xún)H用一個(gè)對象代替。
- 軟件系統不依賴(lài)于這些對象的身份,換言之,這些對象可以是不可分辨的。
滿(mǎn)足以上的這些條件的系統可以使用享元對象。
最后,使用享元模式需要維護一個(gè)記錄了系統已有的所有享元的表,而這需要耗費資源。因此,應當在有足夠多的享元實(shí)例可供共享時(shí)才值得使用享元模式。
八、 享元模式的優(yōu)點(diǎn)和缺點(diǎn)
享元模式的優(yōu)點(diǎn)在于它大幅度地降低內存中對象的數量。但是,它做到這一點(diǎn)所付出的代價(jià)也是很高的:
- 享元模式使得系統更加復雜。為了使對象可以共享,需要將一些狀態(tài)外部化,這使得程序的邏輯復雜化。
- 享元模式將享元對象的狀態(tài)外部化,而讀取外部狀態(tài)使得運行時(shí)間稍微變長(cháng)。