二十三種設計模式[11] - 享元模式(Flyweight Pattern)
摘要
享元模式,物件結構型模式的一種。在《設計模式 - 可複用的面向物件軟體》一書中將之描述為“ 運用共享技術有效地支援大量細粒度的物件 ”。
在享元模式中,通過工廠方法去統一管理一個物件的建立,在建立物件前會嘗試複用工廠類中快取的已建立物件,如果未找到可重用的物件則建立新的物件例項並將其快取。以此來減少物件的數量達到減少記憶體開銷的目的。
在學習享元模式前,需對工廠方法有所瞭解。可參考
結構
- Flyweight(享元介面):所有享元類的介面;
- ConcreteFlyweight(享元):可被共享的類,封裝了自身的內部狀態(工廠判斷快取中是否存在可複用物件的依據,可以被共享但不可被修改);
- UnsharedConcreteFlyweight(非共享享元):不可被共享的類,通常為複合Flyweight物件;
- FlyweightFactory(享元工廠):用來建立並管理Flyweight物件。當用戶請求一個Flyweight時,首先嚐試重用快取中的物件,其次建立新物件;
注:
- 在享元模式中,可以被共享的內容稱為內部狀態
- 在享元模式中,需要使用者設定並且不能被共享的內容稱為外部狀態。
示例
考慮一個畫圖工具,這個畫圖工具提供了各種顏色的圓形和長方形。在設計這個工具時,可能會建立一個Shape介面以及實現了這個介面的Circle和Rectangle類,在Circle和Rectangle類中存在一個Color屬性來標識自身的顏色。當我們使用這個工具畫出100個圖形時,系統中也會分別建立這100個圖形的例項。也就是說,這個工具畫出的圖形數量越多佔用的記憶體越大。並不是一個合理的設計。
現在我們用享元模式重新設計這個畫圖工具。首先,建立一個Shape介面以及介面的實現類Circle和Rectangle。在Circle和Rectangle中存在一個string型別的屬性來標識自身顯示的文字。Shape的例項同一由工廠FlyweightFactory提供,在FlyweightFacatory中存在一個Shape的Dictionary作為享元的快取池,每當客戶向工廠請求Shape的例項時,優先嚐試重用快取池中的例項。具體實現如下。
單純享元模式
public interface IShape { void Draw(ConsoleColor color, int x, int y); } public class Circle : IShape { private string _text = string.Empty; public Circle(string text) { this._text = text; Console.WriteLine($"開始例項化Circle [{text}]"); } public void Draw(ConsoleColor color, int x, int y) { Console.WriteLine($"Circle Draw [Text: {this._text}, Color: {color.ToString()}, 座標x: {x} y: {y}]"); } } public class Rectangle : IShape { private string _text = string.Empty; public Rectangle(string text) { this._text = text; Console.WriteLine($"開始例項化Rectangle [{text}]"); } public void Draw(ConsoleColor color, int x, int y) { Console.WriteLine($"Rectangle Draw [Text: {this._text}, Color: {color.ToString()}, 座標x: {x} y: {y}]"); } } public class FlyweightFactory { private FlyweightFactory() { } private static FlyweightFactory _instance = null; private static object _sysLock = new object(); public static FlyweightFactory Instance { get { if(_instance == null) { lock (_sysLock) { if(_instance == null) { _instance = new FlyweightFactory(); } } } return _instance; } } private Dictionary<string, IShape> _shapePool = new Dictionary<string, IShape>(); public int PoolSize => this._shapePool.Count; public IShape CreateCircle(string text) { IShape shape = this._shapePool.ContainsKey(text) && this._shapePool[text] is Circle ? this._shapePool[text] : null; if(shape == null) { shape = new Circle(text); this._shapePool.Add(text, shape); } return shape; } public IShape CreateRectangle(string text) { IShape shape = this._shapePool.ContainsKey(text) && this._shapePool[text] is Rectangle ? this._shapePool[text] : null; if (shape == null) { shape = new Rectangle(text); this._shapePool.Add(text, shape); } return shape; } } static void Main(string[] args) { IShape circleA = FlyweightFactory.Instance.CreateCircle("很圓的圓形"); IShape circleB = FlyweightFactory.Instance.CreateCircle("很圓的圓形"); IShape rectangleA = FlyweightFactory.Instance.CreateRectangle("很方的長方形"); circleA.Draw(ConsoleColor.Red, 20, 30); circleB.Draw(ConsoleColor.Yellow, 10, 130); rectangleA.Draw(ConsoleColor.Blue, 25, 60); Console.WriteLine($"享元池長度:{FlyweightFactory.Instance.PoolSize}"); Console.WriteLine($"circleA與circleB是否指向同一記憶體地址:{object.ReferenceEquals(circleA, circleB)}"); Console.ReadKey(); }
示例中,string型別的屬性_text作為Circle和Rectangle的內部狀態被共享。當然也可以將Color作為它們的內部狀態,但在示例中Color作為外部狀態使用的目的是為了使不同顏色的圖形依然能夠使用同一物件(只要它們的Text相同)。由於我們只需要一個享元工廠的例項,所以將其設計成單例模式(工作中通常也是這樣做的)。通過執行結果發現,雖然我們通過工廠獲取了兩個Circle類的引用,但它們都指向同一塊記憶體地址。通過這種方式,能夠有效減少記憶體的開銷。
複合享元模式
所謂複合享元,就是將若干個單純享元使用組合模式組合成的非共享享元物件。當我們需要為多個內部狀態不同的享元設定相同的外部狀態時,可以考慮使用複合享元。複合享元本身不可被共享,但其可以分解成單純享元物件。
public interface IShape { void Draw(ConsoleColor color, int x, int y); void Add(IShape shape); } public class Circle : IShape { private string _text = string.Empty; public Circle(string text) { this._text = text; Console.WriteLine($"開始例項化Circle [{text}]"); } public void Draw(ConsoleColor color, int x, int y) { Console.WriteLine($"Circle Draw [Text: {this._text}, Color: {color.ToString()}, 座標x: {x} y: {y}]"); } public void Add(IShape shape) { throw new NotImplementedException("Sorry,I can not execute add function"); } } public class Rectangle : IShape { private string _text = string.Empty; public Rectangle(string text) { this._text = text; Console.WriteLine($"開始例項化Rectangle [{text}]"); } public void Draw(ConsoleColor color, int x, int y) { Console.WriteLine($"Rectangle Draw [Text: {this._text}, Color: {color.ToString()}, 座標x: {x} y: {y}]"); } public void Add(IShape shape) { throw new NotImplementedException("Sorry,I can not execute add function"); } } public class ShapeComposite : IShape { private List<IShape> shapeList = new List<IShape>(); public ShapeComposite() { Console.WriteLine($"開始例項化ShapeComposite"); } public void Draw(ConsoleColor color, int x, int y) { Console.WriteLine($"ShapeComposite Draw"); foreach (var shape in this.shapeList) { shape.Draw(color, x, y); } } public void Add(IShape shape) { if(shape == null) { return; } this.shapeList.Add(shape); } } public class FlyweightFactory { private FlyweightFactory() { } private static FlyweightFactory _instance = null; private static object _sysLock = new object(); public static FlyweightFactory Instance { get { if(_instance == null) { lock (_sysLock) { if(_instance == null) { _instance = new FlyweightFactory(); } } } return _instance; } } private Dictionary<string, IShape> _shapePool = new Dictionary<string, IShape>(); public int PoolSize => this._shapePool.Count; public IShape CreateCircle(string text) { IShape shape = this._shapePool.ContainsKey(text) && this._shapePool[text] is Circle ? this._shapePool[text] : null; if(shape == null) { shape = new Circle(text); this._shapePool.Add(text, shape); } return shape; } public IShape CreateRectangle(string text) { IShape shape = this._shapePool.ContainsKey(text) && this._shapePool[text] is Rectangle ? this._shapePool[text] : null; if (shape == null) { shape = new Rectangle(text); this._shapePool.Add(text, shape); } return shape; } public IShape CreateComposite() { return new ShapeComposite(); } } static void Main(string[] args) { IShape circleA = FlyweightFactory.Instance.CreateCircle("很圓的圓形"); IShape circleB = FlyweightFactory.Instance.CreateCircle("很圓的圓形"); IShape rectangleA = FlyweightFactory.Instance.CreateRectangle("很方的長方形"); Console.WriteLine("--------------"); IShape shapCompositeA = FlyweightFactory.Instance.CreateComposite(); shapCompositeA.Add(circleA); shapCompositeA.Add(rectangleA); shapCompositeA.Draw(ConsoleColor.Yellow, 10, 130); Console.WriteLine("--------------"); IShape shapCompositeB = FlyweightFactory.Instance.CreateComposite(); shapCompositeB.Add(circleA); shapCompositeB.Add(circleB); shapCompositeB.Add(rectangleA); shapCompositeB.Draw(ConsoleColor.Blue, 25, 60); Console.WriteLine("--------------"); Console.WriteLine($"享元池長度:{FlyweightFactory.Instance.PoolSize}"); Console.WriteLine($"shapCompositeA與shapCompositeB是否指向同一記憶體地址:{object.ReferenceEquals(shapCompositeA, shapCompositeB)}"); Console.ReadKey(); }
通過複合享元可以確保其包含的所有單純享元都具有相同的外部狀態,而這些單純享元的內部狀態一般是不相等的(如果相等就沒有使用價值了)。因為這些單純享元是在複合享元被例項化之後注入進去的,也就意味著複合享元的內部狀態(示例中的shapeList屬性)是可變的,因此複合享元不可共享。
享元模式經常和組合模式結合起來表示一個層次結構(葉節點為可共享享元,根節點為非共享享元)。葉節點被共享的結果是,所有的葉節點都不能儲存父節點的引用,而父節點只能作為外部狀態傳給葉節點。
模式補充
string的暫留機制
提到享元模式,就不得不提.Net中string型別(不包含StringBuilder)的暫留機制。與享元模式類似,CLR(公共語言執行時)在其內部維護了一個string型別的快取池,用來儲存使用者建立的string型別引用。一般地,在程式執行的過程中,當用戶建立一個string時,CLR會根據這個string的Hash Code在快取池中查詢相同的string物件。找到則複用這個string物件。沒找到則建立這個物件並將其寫入快取池中。String類中也提供了Intern函式來幫助我們主動在快取池中檢索與該值相等的字串(注意,該函式對比的是string的值而非該值的Hash Code。參考String.Intern)。驗證方式如下。
static void Main(string[] args) { string a = "test"; string b = "test"; string c = "TEST".ToLower(); string d = "te" + "st"; string x = "te"; string y = "st"; string e = x + "st"; string f = x + y; Console.WriteLine(object.ReferenceEquals(a, b)); //True; Console.WriteLine(object.ReferenceEquals(a, c)); //False; Console.WriteLine(object.ReferenceEquals(a, string.Intern(c))); //True; Console.WriteLine(object.ReferenceEquals(a, d)); //True; Console.WriteLine(object.ReferenceEquals(a, e)); //False; Console.WriteLine(object.ReferenceEquals(a, f)); //False; Console.WriteLine(object.ReferenceEquals(e, f)); //False; Console.ReadKey(); }
享元與單例的區別
單例模式:類級別的單例,即一個類只能有一個例項;
享元模式:物件級別的單例,即一個類可擁有多個例項,且多個變數引用同一例項;
在單例模式中類的建立是由類本身去控制的,而類的建立邏輯並不屬於這個類本身的業務邏輯,並且單例模式是嚴格控制其在所有執行緒中只能存在一個例項。而在享元模式中,並不限制這個類的例項數量,只是保證物件在同一內部狀態下只存在一個例項,並且它的例項化過程由享元工廠控制。
總結
當系統中存在大量相同或相似的物件時,使用享,模式能夠有效減少物件的建立,從而達到提高效能、減少記憶體開銷的效果。享元的內部狀態越少可複用的條件也就越少,類的複用次數也就越多,但該模式的難點在於如何合理的分離物件的內部狀態和外部狀態。物件的內部狀態與外部狀態的分離也意味著程式的邏輯更加複雜化。
以上,就是我對享元模式的理解,希望對你有所幫助。
示例原始碼:https://gitee.com/wxingChen/DesignPatternsPractice
系列彙總:https://www.cnblogs.com/wxingchen/p/10031592.html
本文著作權歸本人所有,如需轉載請標明本文連結(https://www.cnblogs.com/wxingchen/p/10078622.html)