享元模式(Flyweight Pattern)
——.NET設計模式系列之十三
Terrylee,2006年3月
摘要:面向對象的思想很好地解決了抽象性的問(wèn)題,一般也不會(huì )出現性能上的問(wèn)題。但是在某些情況下,對象的數量可能會(huì )太多,從而導致了運行時(shí)的代價(jià)。那么我們如何去避免大量細粒度的對象,同時(shí)又不影響客戶(hù)程序使用面向對象的方式進(jìn)行操作?
本文試圖通過(guò)一個(gè)簡(jiǎn)單的字符處理的例子,運用重構的手段,一步步帶你走進(jìn)Flyweight模式,在這個(gè)過(guò)程中我們一同思考、探索、權衡,通過(guò)比較而得出好的實(shí)現方式,而不是給你最終的一個(gè)完美解決方案。
主要內容:
1. Flyweight模式解說(shuō)
2..NET中的Flyweight模式
3.Flyweight模式的實(shí)現要點(diǎn)
……
概述
面向對象的思想很好地解決了抽象性的問(wèn)題,一般也不會(huì )出現性能上的問(wèn)題。但是在某些情況下,對象的數量可能會(huì )太多,從而導致了運行時(shí)的代價(jià)。那么我們如何去避免大量細粒度的對象,同時(shí)又不影響客戶(hù)程序使用面向對象的方式進(jìn)行操作?
意圖
運用共享技術(shù)有效地支持大量細粒度的對象。[GOF 《設計模式》]
結構圖
圖1 Flyweight模式結構圖
生活中的例子
享元模式使用共享技術(shù)有效地支持大量細粒度的對象。公共交換電話(huà)網(wǎng)(PSTN)是享元的一個(gè)例子。有一些資源例如撥號音發(fā)生器、振鈴發(fā)生器和撥號接收器是必須由所有用戶(hù)共享的。當一個(gè)用戶(hù)拿起聽(tīng)筒打電話(huà)時(shí),他不需要知道使用了多少資源。對于用戶(hù)而言所有的事情就是有撥號音,撥打號碼,撥通電話(huà)。
圖2 使用撥號音發(fā)生器例子的享元模式對象圖
Flyweight模式解說(shuō)
Flyweight在拳擊比賽中指最輕量級,即“蠅量級”,這里翻譯為“享元”,可以理解為共享元對象(細粒度對象)的意思。提到Flyweight模式都會(huì )一般都會(huì )用編輯器例子來(lái)說(shuō)明,這里也不例外,但我會(huì )嘗試著(zhù)通過(guò)重構來(lái)看待Flyweight模式??紤]這樣一個(gè)字處理軟件,它需要處理的對象可能有單個(gè)的字符,由字符組成的段落以及整篇文檔,根據面向對象的設計思想和Composite模式,不管是字符還是段落,文檔都應該作為單個(gè)的對象去看待,這里只考慮單個(gè)的字符,不考慮段落及文檔等對象,于是可以很容易的得到下面的結構圖:
圖3
示意性實(shí)現代碼:
// "Charactor"
public abstract class Charactor
{
//Fields
protected char _symbol;
protected int _width;
protected int _height;
protected int _ascent;
protected int _descent;
protected int _pointSize;
//Method
public abstract void Display();
}
// "CharactorA"
public class CharactorA : Charactor
{
// Constructor
public CharactorA()
{
this._symbol = 'A';
this._height = 100;
this._width = 120;
this._ascent = 70;
this._descent = 0;
this._pointSize = 12;
}
//Method
public override void Display()
{
Console.WriteLine(this._symbol);
}
}
// "CharactorB"
public class CharactorB : Charactor
{
// Constructor
public CharactorB()
{
this._symbol = 'B';
this._height = 100;
this._width = 140;
this._ascent = 72;
this._descent = 0;
this._pointSize = 10;
}
//Method
public override void Display()
{
Console.WriteLine(this._symbol);
}
}
// "CharactorC"
public class CharactorC : Charactor
{
// Constructor
public CharactorC()
{
this._symbol = 'C';
this._height = 100;
this._width = 160;
this._ascent = 74;
this._descent = 0;
this._pointSize = 14;
}
//Method
public override void Display()
{
Console.WriteLine(this._symbol);
}
}
好了,現在看到的這段代碼可以說(shuō)是很好地符合了面向對象的思想,但是同時(shí)我們也為此付出了沉重的代價(jià),那就是性能上的開(kāi)銷(xiāo),可以想象,在一篇文檔中,字符的數量遠不止幾百個(gè)這么簡(jiǎn)單,可能上千上萬(wàn),內存中就同時(shí)存在了上千上萬(wàn)個(gè)
Charactor對象,這樣的內存開(kāi)銷(xiāo)是可想而知的。進(jìn)一步分析可以發(fā)現,雖然我們需要的Charactor實(shí)例非常多,這些實(shí)例之間只不過(guò)是狀態(tài)不同而已,也就是說(shuō)這些實(shí)例的狀態(tài)數量是很少的。所以我們并不需要這么多的獨立的Charactor實(shí)例,而只需要為每一種Charactor狀態(tài)創(chuàng )建一個(gè)實(shí)例,讓整個(gè)字符處理軟件共享這些實(shí)例就可以了??催@樣一幅示意圖:
圖4
現在我們看到的A,B,C三個(gè)字符是共享的,也就是說(shuō)如果文檔中任何地方需要這三個(gè)字符,只需要使用共享的這三個(gè)實(shí)例就可以了。然而我們發(fā)現單純的這樣共享也是有問(wèn)題的。雖然文檔中的用到了很多的A字符,雖然字符的symbol等是相同的,它可以共享;但是它們的pointSize卻是不相同的,即字符在文檔中中的大小是不相同的,這個(gè)狀態(tài)不可以共享。為解決這個(gè)問(wèn)題,首先我們將不可共享的狀態(tài)從類(lèi)里面剔除出去,即去掉pointSize這個(gè)狀態(tài)(只是暫時(shí)的J),類(lèi)結構圖如下所示:
圖5
示意性實(shí)現代碼:
// "Charactor"
public abstract class Charactor
{
//Fields
protected char _symbol;
protected int _width;
protected int _height;
protected int _ascent;
protected int _descent;
//Method
public abstract void Display();
}
// "CharactorA"
public class CharactorA : Charactor
{
// Constructor
public CharactorA()
{
this._symbol = 'A';
this._height = 100;
this._width = 120;
this._ascent = 70;
this._descent = 0;
}
//Method
public override void Display()
{
Console.WriteLine(this._symbol);
}
}
// "CharactorB"
public class CharactorB : Charactor
{
// Constructor
public CharactorB()
{
this._symbol = 'B';
this._height = 100;
this._width = 140;
this._ascent = 72;
this._descent = 0;
}
//Method
public override void Display()
{
Console.WriteLine(this._symbol);
}
}
// "CharactorC"
public class CharactorC : Charactor
{
// Constructor
public CharactorC()
{
this._symbol = 'C';
this._height = 100;
this._width = 160;
this._ascent = 74;
this._descent = 0;
}
//Method
public override void Display()
{
Console.WriteLine(this._symbol);
}
}
好,現在類(lèi)里面剩下的狀態(tài)都可以共享了,下面我們要做的工作就是控制
Charactor類(lèi)的創(chuàng )建過(guò)程,即如果已經(jīng)存在了“A”字符這樣的實(shí)例,就不需要再創(chuàng )建,直接返回實(shí)例;如果沒(méi)有,則創(chuàng )建一個(gè)新的實(shí)例。如果把這項工作交給Charactor類(lèi),即Charactor類(lèi)在負責它自身職責的同時(shí)也要負責管理Charactor實(shí)例的管理工作,這在一定程度上有可能違背類(lèi)的單一職責原則,因此,需要一個(gè)單獨的類(lèi)來(lái)做這項工作,引入CharactorFactory類(lèi),結構圖如下:
圖6
示意性實(shí)現代碼:// "CharactorFactory"
public class CharactorFactory
{
// Fields
private Hashtable charactors = new Hashtable();
// Constructor
public CharactorFactory()
{
charactors.Add("A", new CharactorA());
charactors.Add("B", new CharactorB());
charactors.Add("C", new CharactorC());
}
// Method
public Charactor GetCharactor(string key)
{
Charactor charactor = charactors[key] as Charactor;
if (charactor == null)
{
switch (key)
{
case "A": charactor = new CharactorA(); break;
case "B": charactor = new CharactorB(); break;
case "C": charactor = new CharactorC(); break;
//
}
charactors.Add(key, charactor);
}
return charactor;
}
}
到這里已經(jīng)完全解決了可以共享的狀態(tài)(這里很丑陋的一個(gè)地方是出現了
switch語(yǔ)句,但這可以通過(guò)別的辦法消除,為了簡(jiǎn)單期間我們先保持這種寫(xiě)法)。下面的工作就是處理剛才被我們剔除出去的那些不可共享的狀態(tài),因為雖然將那些狀態(tài)移除了,但是Charactor對象仍然需要這些狀態(tài),被我們剝離后這些對象根本就無(wú)法工作,所以需要將這些狀態(tài)外部化。首先會(huì )想到一種比較簡(jiǎn)單的解決方案就是對于不能共享的那些狀態(tài),不需要去在Charactor類(lèi)中設置,而直接在客戶(hù)程序代碼中進(jìn)行設置,類(lèi)結構圖如下:
圖7
示意性實(shí)現代碼:
public class Program
{
public static void Main()
{
Charactor ca = new CharactorA();
Charactor cb = new CharactorB();
Charactor cc = new CharactorC();
//顯示字符
//設置字符的大小ChangeSize();
}
public void ChangeSize()
{
//在這里設置字符的大小
}
}
按照這樣的實(shí)現思路,可以發(fā)現如果有多個(gè)客戶(hù)端程序使用的話(huà),會(huì )出現大量的重復性的邏輯,用重構的術(shù)語(yǔ)來(lái)說(shuō)是出現了代碼的壞味道,不利于代碼的復用和維護;另外把這些狀態(tài)和行為移到客戶(hù)程序里面破壞了封裝性的原則。再次轉變我們的實(shí)現思路,可以確定的是這些狀態(tài)仍然屬于
Charactor對象,所以它還是應該出現在Charactor類(lèi)中,對于不同的狀態(tài)可以采取在客戶(hù)程序中通過(guò)參數化的方式傳入。類(lèi)結構圖如下:
圖8
示意性實(shí)現代碼:
// "Charactor"
public abstract class Charactor
{
//Fields
protected char _symbol;
protected int _width;
protected int _height;
protected int _ascent;
protected int _descent;
protected int _pointSize;
//Method
public abstract void SetPointSize(int size);
public abstract void Display();
}
// "CharactorA"
public class CharactorA : Charactor
{
// Constructor
public CharactorA()
{
this._symbol = 'A';
this._height = 100;
this._width = 120;
this._ascent = 70;
this._descent = 0;
}
//Method
public override void SetPointSize(int size)
{
this._pointSize = size;
}
public override void Display()
{
Console.WriteLine(this._symbol +
"pointsize:" + this._pointSize);
}
}
// "CharactorB"
public class CharactorB : Charactor
{
// Constructor
public CharactorB()
{
this._symbol = 'B';
this._height = 100;
this._width = 140;
this._ascent = 72;
this._descent = 0;
}
//Method
public override void SetPointSize(int size)
{
this._pointSize = size;
}
public override void Display()
{
Console.WriteLine(this._symbol +
"pointsize:" + this._pointSize);
}
}
// "CharactorC"
public class CharactorC : Charactor
{
// Constructor
public CharactorC()
{
this._symbol = 'C';
this._height = 100;
this._width = 160;
this._ascent = 74;
this._descent = 0;
}
//Method
public override void SetPointSize(int size)
{
this._pointSize = size;
}
public override void Display()
{
Console.WriteLine(this._symbol +
"pointsize:" + this._pointSize);
}
}
// "CharactorFactory"
public class CharactorFactory
{
// Fields
private Hashtable charactors = new Hashtable();
// Constructor
public CharactorFactory()
{
charactors.Add("A", new CharactorA());
charactors.Add("B", new CharactorB());
charactors.Add("C", new CharactorC());
}
// Method
public Charactor GetCharactor(string key)
{
Charactor charactor = charactors[key] as Charactor;
if (charactor == null)
{
switch (key)
{
case "A": charactor = new CharactorA(); break;
case "B": charactor = new CharactorB(); break;
case "C": charactor = new CharactorC(); break;
//
}
charactors.Add(key, charactor);
}
return charactor;
}
}
public class Program
{
public static void Main()
{
CharactorFactory factory = new CharactorFactory();
// Charactor "A"
CharactorA ca = (CharactorA)factory.GetCharactor("A");
ca.SetPointSize(12);
ca.Display();
// Charactor "B"
CharactorB cb = (CharactorB)factory.GetCharactor("B");
ca.SetPointSize(10);
ca.Display();
// Charactor "C"
CharactorC cc = (CharactorC)factory.GetCharactor("C");
ca.SetPointSize(14);
ca.Display();
}
}
可以看到這樣的實(shí)現明顯優(yōu)于第一種實(shí)現思路。好了,到這里我們就到到了通過(guò)
Flyweight模式實(shí)現了優(yōu)化資源的這樣一個(gè)目的。在這個(gè)過(guò)程中,還有如下幾點(diǎn)需要說(shuō)明:
1.引入CharactorFactory是個(gè)關(guān)鍵,在這里創(chuàng )建對象已經(jīng)不是new一個(gè)Charactor對象那么簡(jiǎn)單,而必須用工廠(chǎng)方法封裝起來(lái)。
2.在這個(gè)例子中把Charactor對象作為Flyweight對象是否準確值的考慮,這里只是為了說(shuō)明Flyweight模式,至于在實(shí)際應用中,哪些對象需要作為Flyweight對象是要經(jīng)過(guò)很好的計算得知,而絕不是憑空臆想。
3.區分內外部狀態(tài)很重要,這是享元對象能做到享元的關(guān)鍵所在。
到這里,其實(shí)我們的討論還沒(méi)有結束。有人可能會(huì )提出如下問(wèn)題,享元對象(Charactor)在這個(gè)系統中相對于每一個(gè)內部狀態(tài)而言它是唯一的,這跟單件模式有什么區別呢?這個(gè)問(wèn)題已經(jīng)很好回答了,那就是單件類(lèi)是不能直接被實(shí)例化的,而享元類(lèi)是可以被實(shí)例化的。事實(shí)上在這里面真正被設計為單件的應該是享元工廠(chǎng)(不是享元)類(lèi),因為如果創(chuàng )建很多個(gè)享元工廠(chǎng)的實(shí)例,那我們所做的一切努力都是白費的,并沒(méi)有減少對象的個(gè)數。修改后的類(lèi)結構圖如下:
圖9
示意性實(shí)現代碼:
// "CharactorFactory"
public class CharactorFactory
{
// Fields
private Hashtable charactors = new Hashtable();
private CharactorFactory instance;
// Constructor
private CharactorFactory()
{
charactors.Add("A", new CharactorA());
charactors.Add("B", new CharactorB());
charactors.Add("C", new CharactorC());
}
// Property
public CharactorFactory Instance
{
get
{
if (instance != null)
{
instance = new CharactorFactory();
}
return instance;
}
}
// Method
public Charactor GetCharactor(string key)
{
Charactor charactor = charactors[key] as Charactor;
if (charactor == null)
{
switch (key)
{
case "A": charactor = new CharactorA(); break;
case "B": charactor = new CharactorB(); break;
case "C": charactor = new CharactorC(); break;
//
}
charactors.Add(key, charactor);
}
return charactor;
}
}
.NET框架中的Flyweight
Flyweight更多時(shí)候的時(shí)候一種底層的設計模式,在我們的實(shí)際應用程序中使用的并不是很多。在.NET中的String類(lèi)型其實(shí)就是運用了Flyweight模式??梢韵胂?,如果每次執行string s1 = “abcd”操作,都創(chuàng )建一個(gè)新的字符串對象的話(huà),內存的開(kāi)銷(xiāo)會(huì )很大。所以.NET中如果第一次創(chuàng )建了這樣的一個(gè)字符串對象s1,下次再創(chuàng )建相同的字符串s2時(shí)只是把它的引用指向“abcd”,這樣就實(shí)現了“abcd”在內存中的共享??梢酝ㄟ^(guò)下面一個(gè)簡(jiǎn)單的程序來(lái)演示s1和s2的引用是否一致:
public class Program
{
public static void Main(string[] args)
{
string s1 = "abcd";
string s2 = "abcd";
Console.WriteLine(Object.ReferenceEquals(s1,s2));
Console.ReadLine();
}
}
可以看到,輸出的結果為
True。但是大家要注意的是如果再有一個(gè)字符串s3,它的初始值為“ab”,再對它進(jìn)行操作s3 = s3 + “cd”,這時(shí)雖然s1和s3的值相同,但是它們的引用是不同的。關(guān)于String的詳細情況大家可以參考SDK,這里不再討論了。
效果及實(shí)現要點(diǎn)
1.面向對象很好的解決了抽象性的問(wèn)題,但是作為一個(gè)運行在機器中的程序實(shí)體,我們需要考慮對象的代價(jià)問(wèn)題。Flyweight設計模式主要解決面向對象的代價(jià)問(wèn)題,一般不觸及面向對象的抽象性問(wèn)題。
2.Flyweight采用對象共享的做法來(lái)降低系統中對象的個(gè)數,從而降低細粒度對象給系統帶來(lái)的內存壓力。在具體實(shí)現方面,要注意對象狀態(tài)的處理。
3.享元模式的優(yōu)點(diǎn)在于它大幅度地降低內存中對象的數量。但是,它做到這一點(diǎn)所付出的代價(jià)也是很高的:享元模式使得系統更加復雜。為了使對象可以共享,需要將一些狀態(tài)外部化,這使得程序的邏輯復雜化。另外它將享元對象的狀態(tài)外部化,而讀取外部狀態(tài)使得運行時(shí)間稍微變長(cháng)。
適用性
當以下所有的條件都滿(mǎn)足時(shí),可以考慮使用享元模式:
1、 一個(gè)系統有大量的對象。
2、 這些對象耗費大量的內存。
3、 這些對象的狀態(tài)中的大部分都可以外部化。
4、 這些對象可以按照內蘊狀態(tài)分成很多的組,當把外蘊對象從對象中剔除時(shí),每一個(gè)組都可以?xún)H用一個(gè)對象代替。
5、 軟件系統不依賴(lài)于這些對象的身份,換言之,這些對象可以是不可分辨的。
滿(mǎn)足以上的這些條件的系統可以使用享元對象。最后,使用享元模式需要維護一個(gè)記錄了系統已有的所有享元的表,而這需要耗費資源。因此,應當在有足夠多的享元實(shí)例可供共享時(shí)才值得使用享元模式。
總結
Flyweight模式解決的是由于大量的細粒度對象所造成的內存開(kāi)銷(xiāo)的問(wèn)題,它在實(shí)際的開(kāi)發(fā)中并不常用,但是作為底層的提升性能的一種手段卻很有效。