本文試圖通過一個簡單的字符處理的例子,運用重構的手段,一步步帶你走進Flyweight模式,在這個過程中我們一同思考、探索、權衡,通過比較而得出好的實現方式,而不是給你最終的一個完美解決方案。
主要內容:
1.Flyweight模式解說
2..NET中的Flyweight模式
3.Flyweight模式的實現要點
……
概述
面向對象的思想很好地解決了抽象性的問題,一般也不會出現性能上的問題。但是在某些情況下,對象的數量可能會太多,從而導致了運行時的代價。那麼我們如何去避免大量細粒度的對象,同時又不影響客戶程序使用面向對象的方式進行操作?
意圖
運用共享技術有效地支持大量細粒度的對象。[GOF 《設計模式》]
結構圖
圖1 Flyweight模式結構圖
生活中的例子
享元模式使用共享技術有效地支持大量細粒度的對象。公共交換電話網(PSTN)是享元的一個例子。有一些資源例如撥號音發生器、振鈴發生器和撥號接收器是必須由所有用戶共享的。當一個用戶拿起聽筒打電話時,他不需要知道使用了多少資源。對於用戶而言所有的事情就是有撥號音,撥打號碼,撥通電話。
圖2 使用撥號音發生器例子的享元模式對象圖
Flyweight模式解說
Flyweight在拳擊比賽中指最輕量級,即“蠅量級”,這裡翻譯為“享元”,可以理解為共享元對象(細粒度對象)的意思。提到Flyweight模式都會一般都會用編輯器例子來說明,這裡也不例外,但我會嘗試著通過重構來看待Flyweight模式。考慮這樣一個字處理軟件,它需要處理的對象可能有單個的字符,由字符組成的段落以及整篇文檔,根據面向對象的設計思想和Composite模式,不管是字符還是段落,文檔都應該作為單個的對象去看待,這裡只考慮單個的字符,不考慮段落及文檔等對象,於是可以很容易的得到下面的結構圖:
圖3
示意性實現代碼:
// "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);
}
}
好了,現在看到的這段代碼可以說是很好地符合了面向對象的思想,但是同時我們也為此付出了沉重的代價,那就是性能上的開銷,可以想象,在一篇文檔中,字符的數量遠不止幾百個這麼簡單,可能上千上萬,內存中就同時存在了上千上萬個Charactor對象,這樣的內存開銷是可想而知的。進一步分析可以發現,雖然我們需要的Charactor實例非常多,這些實例之間只不過是狀態不同而已,也就是說這些實例的狀態數量是很少的。所以我們並不需要這麼多的獨立的Charactor實例,而只需要為每一種Charactor狀態創建一個實例,讓整個字符處理軟件共享這些實例就可以了。看這樣一幅示意圖:
圖4
現在我們看到的A,B,C三個字符是共享的,也就是說如果文檔中任何地方需要這三個字符,只需要使用共享的這三個實例就可以了。然而我們發現單純的這樣共享也是有問題的。雖然文檔中的用到了很多的A字符,雖然字符的symbol等是相同的,它可以共享;但是它們的pointSize卻是不相同的,即字符在文檔中中的大小是不相同的,這個狀態不可以共享。為解決這個問題,首先我們將不可共享的狀態從類裡面剔除出去,即去掉pointSize這個狀態(只是暫時的J),類結構圖如下所示:
圖5
示意性實現代碼:
// "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);
}
}
好,現在類裡面剩下的狀態都可以共享了,下面我們要做的工作就是控制Charactor類的創建過程,即如果已經存在了“A”字符這樣的實例,就不需要再創建,直接返回實例;如果沒有,則創建一個新的實例。如果把這項工作交給Charactor類,即Charactor類在負責它自身職責的同時也要負責管理Charactor實例的管理工作,這在一定程度上有可能違背類的單一職責原則,因此,需要一個單獨的類來做這項工作,引入CharactorFactory類,結構圖如下:
圖6
示意性實現代碼:
// "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;
}
}
到這裡已經完全解決了可以共享的狀態(這裡很丑陋的一個地方是出現了switch語句,但這可以通過別的辦法消除,為了簡單期間我們先保持這種寫法)。下面的工作就是處理剛才被我們剔除出去的那些不可共享的狀態,因為雖然將那些狀態移除了,但是Charactor對象仍然需要這些狀態,被我們剝離後這些對象根本就無法工作,所以需要將這些狀態外部化。首先會想到一種比較簡單的解決方案就是對於不能共享的那些狀態,不需要去在Charactor類中設置,而直接在客戶程序代碼中進行設置,類結構圖如下:
圖7
示意性實現代碼:
public class Program
{
public static void Main()
{
Charactor ca = new CharactorA();
Charactor cb = new CharactorB();
Charactor cc = new CharactorC();
//顯示字符
//設置字符的大小ChangeSize();
}
public void ChangeSize()
{
//在這裡設置字符的大小
}
}
按照這樣的實現思路,可以發現如果有多個客戶端程序使用的話,會出現大量的重復性的邏輯,用重構的術語來說是出現了代碼的壞味道,不利於代碼的復用和維護;另外把這些狀態和行為移到客戶程序裡面破壞了封裝性的原則。再次轉變我們的實現思路,可以確定的是這些狀態仍然屬於Charactor對象,所以它還是應該出現在Charactor類中,對於不同的狀態可以采取在客戶程序中通過參數化的方式傳入。類結構圖如下:
圖8
示意性實現代碼:
// "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();
}
}
可以看到這樣的實現明顯優於第一種實現思路。好了,到這裡我們就到到了通過Flyweight模式實現了優化資源的這樣一個目的。在這個過程中,還有如下幾點需要說明:
1.引入CharactorFactory是個關鍵,在這裡創建對象已經不是new一個Charactor對象那麼簡單,而必須用工廠方法封裝起來。
2.在這個例子中把Charactor對象作為Flyweight對象是否准確值的考慮,這裡只是為了說明Flyweight模式,至於在實際應用中,哪些對象需要作為Flyweight對象是要經過很好的計算得知,而絕不是憑空臆想。
3.區分內外部狀態很重要,這是享元對象能做到享元的關鍵所在。
到這裡,其實我們的討論還沒有結束。有人可能會提出如下問題,享元對象(Charactor)在這個系統中相對於每一個內部狀態而言它是唯一的,這跟單件模式有什麼區別呢?這個問題已經很好回答了,那就是單件類是不能直接被實例化的,而享元類是可以被實例化的。事實上在這裡面真正被設計為單件的應該是享元工廠(不是享元)類,因為如果創建很多個享元工廠的實例,那我們所做的一切努力都是白費的,並沒有減少對象的個數。修改後的類結構圖如下:
圖9
示意性實現代碼:
// "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更多時候的時候一種底層的設計模式,在我們的實際應用程序中使用的並不是很多。在.NET中的String類型其實就是運用了Flyweight模式。可以想象,如果每次執行string s1 = “abcd”操作,都創建一個新的字符串對象的話,內存的開銷會很大。所以.NET中如果第一次創建了這樣的一個字符串對象s1,下次再創建相同的字符串s2時只是把它的引用指向“abcd”,這樣就實現了“abcd”在內存中的共享。可以通過下面一個簡單的程序來演示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。但是大家要注意的是如果再有一個字符串s3,它的初始值為“ab”,再對它進行操作s3 = s3 + “cd”,這時雖然s1和s3的值相同,但是它們的引用是不同的。關於String的詳細情況大家可以參考SDK,這裡不再討論了。
效果及實現要點
1.面向對象很好的解決了抽象性的問題,但是作為一個運行在機器中的程序實體,我們需要考慮對象的代價問題。Flyweight設計模式主要解決面向對象的代價問題,一般不觸及面向對象的抽象性問題。
2.Flyweight采用對象共享的做法來降低系統中對象的個數,從而降低細粒度對象給系統帶來的內存壓力。在具體實現方面,要注意對象狀態的處理。
3.享元模式的優點在於它大幅度地降低內存中對象的數量。但是,它做到這一點所付出的代價也是很高的:享元模式使得系統更加復雜。為了使對象可以共享,需要將一些狀態外部化,這使得程序的邏輯復雜化。另外它將享元對象的狀態外部化,而讀取外部狀態使得運行時間稍微變長。
適用性
當以下所有的條件都滿足時,可以考慮使用享元模式:
1、一個系統有大量的對象。
2、這些對象耗費大量的內存。
3、這些對象的狀態中的大部分都可以外部化。
4、這些對象可以按照內蘊狀態分成很多的組,當把外蘊對象從對象中剔除時,每一個組都可以僅用一個對象代替。
5、軟件系統不依賴於這些對象的身份,換言之,這些對象可以是不可分辨的。
滿足以上的這些條件的系統可以使用享元對象。最後,使用享元模式需要維護一個記錄了系統已有的所有享元的表,而這需要耗費資源。因此,應當在有足夠多的享元實例可供共享時才值得使用享元模式。
總結
Flyweight模式解決的是由於大量的細粒度對象所造成的內存開銷的問題,它在實際的開發中並不常用,但是作為底層的提升性能的一種手段卻很有效。