享元模式 是結構型設計模式中專門解決「大量細粒度物件記憶體壓力」的模式。其核心思想:**區分內部狀態 (可共享) 與外部狀態 (不可共享),透過共享內部狀態大幅減少物件數量**。適用於文字編輯器字元、圖形編輯器圖元、遊戲粒子系統、連線池、執行緒池等大量相似物件場景。
| 特性 | 內部狀態 | 外部狀態 |
|---|---|---|
| 定義 | 物件固有、不隨環境改變的資料 | 隨環境、情境變化的資料 |
| 共享性 | 可跨物件共享 | 不可共享,由客戶端管理 |
| 儲存位置 | 享元物件內部 | 客戶端傳入方法參數 |
| 範例 (文字編輯器) | 字元碼、字型、字體大小 | 位置 (x,y)、顏色、旋轉角度 |
| 範例 (圖形編輯器) | 圖形類型、幾何定義 | 座標、縮放、顏色、圖層 |
// 享元介面
public interface IFlyweight
{
void Operation(int extrinsicState); // 外部狀態由參數傳入
}
// 具體享元:儲存內部狀態
public class ConcreteFlyweight : IFlyweight
{
private readonly string _intrinsicState; // 內部狀態 (唯讀、共享)
public ConcreteFlyweight(string intrinsicState)
{
_intrinsicState = intrinsicState;
}
public void Operation(int extrinsicState)
{
Console.WriteLine($"ConcreteFlyweight: 內部狀態={_intrinsicState}, 外部狀態={extrinsicState}");
}
}
// 享元工廠:管理享元池,確保共享
public class FlyweightFactory
{
private readonly Dictionary _flyweights = new();
public IFlyweight GetFlyweight(string key)
{
if (!_flyweights.TryGetValue(key, out var flyweight))
{
flyweight = new ConcreteFlyweight(key);
_flyweights[key] = flyweight;
Console.WriteLine($"[Factory] 建立新享元: {key}");
}
else
{
Console.WriteLine($"[Factory] 重用享元: {key}");
}
return flyweight;
}
public int Count => _flyweights.Count;
}
// 客戶端使用
var factory = new FlyweightFactory();
IFlyweight fx = factory.GetFlyweight("A");
fx.Operation(1); // 外部狀態由客戶端提供
IFlyweight fy = factory.GetFlyweight("B");
fy.Operation(2);
IFlyweight fz = factory.GetFlyweight("A"); // 重用 "A"
fz.Operation(3);
Console.WriteLine($"總享元數: {factory.Count}"); // 輸出 2
一份 10 萬字的文件,若每個字元都是獨立物件,將建立 10 萬個物件。實際上只有約 100 個不同字元 (ASCII/常用中文),其餘差異僅在「位置」與「樣式」。
// 內部狀態:字元碼、字型家族
public record CharacterIntrinsic(char Char, string FontFamily, float FontSize);
// 外部狀態:位置、顏色、樣式
public record CharacterExtrinsic(int X, int Y, ConsoleColor Color, bool Bold, bool Italic);
// 享元介面
public interface ICharacterFlyweight
{
void Render(CharacterExtrinsic extrinsic);
}
// 具體享元
public class CharacterFlyweight : ICharacterFlyweight
{
private readonly CharacterIntrinsic _intrinsic;
public CharacterFlyweight(CharacterIntrinsic intrinsic)
{
_intrinsic = intrinsic;
}
public void Render(CharacterExtrinsic extrinsic)
{
// 實際渲染邏輯:使用 _intrinsic.Char, _intrinsic.FontFamily 等
// 結合 extrinsic.X, extrinsic.Y, extrinsic.Color 繪製
Console.WriteLine($"渲染字元: '{_intrinsic.Char}' at ({extrinsic.X},{extrinsic.Y}) " +
$"Font={_intrinsic.FontFamily} Size={_intrinsic.FontSize} " +
$"Color={extrinsic.Color} Bold={extrinsic.Bold}");
}
}
// 享元工廠
public class CharacterFactory
{
private readonly Dictionary _pool = new();
public ICharacterFlyweight GetCharacter(char c, string fontFamily = "Consolas", float fontSize = 12f)
{
var key = new CharacterIntrinsic(c, fontFamily, fontSize);
return _pool.GetValueOrDefault(key) ?? (_pool[key] = new CharacterFlyweight(key));
}
public int PoolSize => _pool.Count;
}
// 文件模型:只儲存外部狀態序列
public class Document
{
private readonly List<(ICharacterFlyweight Flyweight, CharacterExtrinsic Extrinsic)> _chars = new();
private readonly CharacterFactory _factory = new();
public void AddChar(char c, int x, int y, ConsoleColor color = ConsoleColor.White)
{
var flyweight = _factory.GetCharacter(c);
var extrinsic = new CharacterExtrinsic(x, y, color, false, false);
_chars.Add((flyweight, extrinsic));
}
public void Render()
{
foreach (var (fw, ext) in _chars)
fw.Render(ext);
}
public void Stats()
{
Console.WriteLine($"文件字元數: {_chars.Count}, 享元池大小: {_factory.PoolSize}");
Console.WriteLine($"記憶體節省: ~{(_chars.Count - _factory.PoolSize) * 100 / _chars.Count}%");
}
}
// 使用
var doc = new Document();
string text = "Hello Flyweight Pattern! ";
for (int i = 0; i < text.Length; i++)
doc.AddChar(text[i], i * 10, 0);
doc.Render();
doc.Stats();
// 輸出: 文件字元數: 25, 享元池大小: 18, 記憶體節省: ~28%
// 飲品內部狀態:名稱、配方、基礎價格
public record CoffeeIntrinsic(string Name, string Recipe, decimal BasePrice);
// 享元介面
public interface ICoffeeFlyweight
{
void Serve(int orderNumber); // 外部狀態: 訂單號/桌號
}
// 具體享元
public class CoffeeFlyweight : ICoffeeFlyweight
{
private readonly CoffeeIntrinsic _intrinsic;
public CoffeeFlyweight(CoffeeIntrinsic intrinsic) => _intrinsic = intrinsic;
public void Serve(int orderNumber)
{
Console.WriteLine($"[訂單 #{orderNumber}] 提供 {_intrinsic.Name} - 配方: {_intrinsic.Recipe} - 價格: ${_intrinsic.BasePrice}");
}
}
// 工廠
public class CoffeeFactory
{
private readonly Dictionary _coffees = new();
public ICoffeeFlyweight GetCoffee(string name)
{
return _coffees.GetValueOrDefault(name) ?? (_coffees[name] = new CoffeeFlyweight(
name switch
{
"Black Coffee" => new CoffeeIntrinsic("Black Coffee", "Espresso + Hot Water", 50m),
"Cappuccino" => new CoffeeIntrinsic("Cappuccino", "Espresso + Steamed Milk + Foam", 80m),
"Latte" => new CoffeeIntrinsic("Latte", "Espresso + Steamed Milk", 75m),
"Espresso" => new CoffeeIntrinsic("Espresso", "Pure Espresso Shot", 60m),
_ => throw new ArgumentException($"未知飲品: {name}")
}));
}
public int VarietyCount => _coffees.Count;
}
// 客戶端
var factory = new CoffeeFactory();
int orderNo = 0;
string[] orders = { "Black Coffee", "Cappuccino", "Espresso", "Cappuccino", "Latte", "Black Coffee", "Espresso" };
foreach (string coffeeName in orders)
{
factory.GetCoffee(coffeeName).Serve(++orderNo);
}
Console.WriteLine($"總訂單: {orderNo}, 飲品種類: {factory.VarietyCount}");
// 輸出: 總訂單: 7, 飲品種類: 4 (只建立 4 個飲品物件)
// 外部狀態:桌號、加糖、加冰、特殊要求
public record CoffeeExtrinsic(int TableNumber, bool Sugar, bool Ice, string SpecialRequest);
// 享元介面調整
public interface ICoffeeFlyweight
{
void Serve(CoffeeExtrinsic extrinsic);
}
// 具體享元調整
public class CoffeeFlyweight : ICoffeeFlyweight
{
private readonly CoffeeIntrinsic _intrinsic;
public CoffeeFlyweight(CoffeeIntrinsic intrinsic) => _intrinsic = intrinsic;
public void Serve(CoffeeExtrinsic extrinsic)
{
Console.WriteLine($"[桌號 {extrinsic.TableNumber}] {_intrinsic.Name} " +
$"(糖: {extrinsic.Sugar}, 冰: {extrinsic.Ice}) " +
$"{(string.IsNullOrEmpty(extrinsic.SpecialRequest) ? "" : $"備註: {extrinsic.SpecialRequest}")}");
}
}
// 使用
var factory = new CoffeeFactory();
var table1 = new CoffeeExtrinsic(1, true, false, "少冰");
var table2 = new CoffeeExtrinsic(2, false, true, "");
factory.GetCoffee("Latte").Serve(table1);
factory.GetCoffee("Latte").Serve(table2);
// 同一個 Latte 享元服務不同桌號,外部狀態由客戶端傳入
將多個單純享元組合成不可共享的複合物件,複合物件本身不共享,但內部組件共享:
// 複合享元:一行文字 (由多個字元享元組成)
public class TextLineFlyweight : IFlyweight
{
private readonly List _characters;
public TextLineFlyweight(IEnumerable characters)
{
_characters = characters.ToList();
}
public void Operation(int extrinsicState) // extrinsicState 可為行號、縮排等
{
int x = extrinsicState * 20; // 簡化:行號轉 x 座標
foreach (var ch in _characters)
{
ch.Render(new CharacterExtrinsic(x, 0, ConsoleColor.White, false, false));
x += 10; // 字元寬度
}
}
}
當 **所有** 以下條件成立時,考慮使用享元模式:
| 優點 | 缺點 |
|---|---|
| 大幅減少記憶體使用 (常達 50-90%) | 增加系統複雜度 (需區分內/外部狀態) |
| 減少 GC 壓力 (物件數量少) | 執行期稍慢 (需查找工廠、傳遞外部狀態) |
| 符合單一職責 (狀態分離) | 邏輯分散 (客戶端需管理外部狀態) |
| 天然支援不可變物件設計 | 除錯困難 (共享物件難以追蹤特定實例) |
string.Intern() 將相同字串存入全域表,相同內容共享同一實例| 方案 | 適用場景 | 優點 |
|---|---|---|
| Object Pool (物件池) | 頻繁建立/銷毀的昂貴物件 (資料庫連線、執行緒、大型緩衝區) | 重用實例、可控制池大小、支援非同步 |
| Value Type (struct) | 小型、不可變、大量資料 (座標、顏色、矩陣) | 零 GC、堆疊配置、記憶體緊湊 |
| Array/Span/Memory | 大量同質資料處理 | 連續記憶體、SIMD 友善、零複製切片 |
| String Interning | 大量重複字串 | 內建、透明、跨 AppDomain |
享元模式是記憶體受限環境、大量細粒度物件系統的利器。其精髓在於 **「狀態分離」與「共享管理」**。現代 .NET 開發中,若面臨大量物件記憶體壓力,優先考慮:
struct / record struct (值型別)ArrayPool / MemoryPool / 物件池理解享元模式不僅能解決記憶體問題,更能訓練「狀態分離」的架構思維,這在雲端原生、高併發系統設計中極具價值。