引子:項目多也別傻做#
小菜接了私營業主的小型外包:產品展示網站、博客、新聞發布等等。每來一位客戶就租一個虛擬空間、複製一份程式碼。
100 家企業 = 100 個空間 + 100 套資料庫 + 100 份重複程式碼。
任何 Bug 修正或需求變更,維護量等比放大。
解法:把這些網站的核心程式碼共享,透過 ID 區分不同用戶——大型博客或電商平台都是這麼做的。
享元模式#
享元模式(Flyweight Pattern):運用共享技術有效地支援大量細粒度的物件。
當系統有大量相似物件時,把多個物件共用的部分抽取出來,形成共享物件,可以大幅減少實例數量、節約記憶體。
結構#
- Flyweight:所有具體享元類的超類或介面,可接受並作用於外部狀態
- ConcreteFlyweight:實作 Flyweight 介面,並為內部狀態增加儲存空間
- UnsharedConcreteFlyweight:那些不需要共享的 Flyweight 子類;介面共享是可能但不強制
- FlyweightFactory(享元工廠):建立並管理 Flyweight 物件,確保合理共享。當客戶請求一個 Flyweight 時,提供已有的實例或新建一個
classDiagram
class FlyweightFactory {
-Hashtable flyweights
+GetFlyweight(key) Flyweight
}
class Flyweight {
<<abstract>>
+Operation(extrinsicstate)*
}
class ConcreteFlyweight
class UnsharedConcreteFlyweight
class Client
Client --> FlyweightFactory
Client ..> UnsharedConcreteFlyweight
FlyweightFactory o--> Flyweight
Flyweight <|-- ConcreteFlyweight
Flyweight <|-- UnsharedConcreteFlyweightabstract class Flyweight
{
public abstract void Operation(int extrinsicstate);
}
class ConcreteFlyweight : Flyweight
{
public override void Operation(int extrinsicstate)
=> Console.WriteLine("具體 Flyweight:" + extrinsicstate);
}
class FlyweightFactory
{
private Hashtable flyweights = new Hashtable();
public Flyweight GetFlyweight(string key)
{
if (!flyweights.ContainsKey(key))
flyweights.Add(key, new ConcreteFlyweight());
return (Flyweight)flyweights[key];
}
}內部狀態 vs. 外部狀態#
- 內部狀態(Intrinsic State):在享元物件內部、不會隨環境改變的共享部分。儲存在 ConcreteFlyweight 物件中
- 外部狀態(Extrinsic State):隨環境改變、不可共享的狀態。應由客戶端對象儲存或計算,呼叫 Flyweight 操作時將該狀態傳入
享元模式可以避免大量非常相似類的開銷——把相同部分共享、不同部分外部化。
範例:共享網站程式碼#
public class User
{
public string Name { get; }
public User(string name) { Name = name; }
}
abstract class WebSite
{
public abstract void Use(User user);
}
class ConcreteWebSite : WebSite
{
private string name;
public ConcreteWebSite(string name) { this.name = name; }
public override void Use(User user)
=> Console.WriteLine($"網站分類:{name} 用戶:{user.Name}");
}
class WebSiteFactory
{
private Hashtable flyweights = new Hashtable();
public WebSite GetWebSiteCategory(string key)
{
if (!flyweights.ContainsKey(key))
flyweights.Add(key, new ConcreteWebSite(key));
return (WebSite)flyweights[key];
}
public int GetWebSiteCount() => flyweights.Count;
}客戶端:
WebSiteFactory f = new WebSiteFactory();
WebSite fx = f.GetWebSiteCategory("產品展示"); fx.Use(new User("小菜"));
WebSite fy = f.GetWebSiteCategory("產品展示"); fy.Use(new User("大鳥"));
WebSite fz = f.GetWebSiteCategory("產品展示"); fz.Use(new User("嬌嬌"));
WebSite fl = f.GetWebSiteCategory("博客"); fl.Use(new User("老瑞童"));
WebSite fm = f.GetWebSiteCategory("博客"); fm.Use(new User("桃谷六仙"));
WebSite fn = f.GetWebSiteCategory("博客"); fn.Use(new User("南海鱷神"));
Console.WriteLine($"網站分類總數為 {f.GetWebSiteCount()}");
// 輸出:網站分類總數為 26 個用戶共享 2 個網站實例。內部狀態(網站分類)共享,外部狀態(用戶)由客戶端傳入。
.NET 中的享元#
.NET 的
string就是享元模式的應用:string titleA = "大話設計模式"; string titleB = "大話設計模式"; Console.WriteLine(Object.ReferenceEquals(titleA, titleB)); // True第一次建立
titleA時 .NET 已將字串放入字串駐留池(intern pool);建立titleB時直接指向同一實例,不重複佔用記憶體。
棋類遊戲的享元#
圍棋、五子棋、跳棋有大量棋子物件:
- 內部狀態:顏色(圍棋、五子棋只有黑白;跳棋顏色不多)
- 外部狀態:方位座標
一盤圍棋理論上有 361 個位置。常規寫法每盤可能有兩三百個棋子物件,伺服器很難支撐多人對局。
用享元模式後,棋子物件可以減少到只有兩個實例,記憶體開銷大幅降低。
何時使用?#
- 應用程式使用了大量物件,且造成很大儲存開銷
- 物件的大多數狀態可以外部化——刪除外部狀態後,可用相對較少的共享物件取代很多組物件
缺點#
- 需要維護一個記錄系統已有享元的列表,本身耗費資源
- 享元模式使系統更複雜:需要將狀態外部化,會讓邏輯複雜化
應當在有足夠多的對象實例可供共享時才值得使用享元模式。
兩三個個人博客網站不必考慮享元;要做開放註冊的博客平台,享元就是非常好的選擇。