Flyweight Pattern(享元模式)
是什麼?
Flyweight Pattern 運用共享技術來有效支援大量細粒度的物件。它將物件的狀態分為可共享的內在狀態(Intrinsic State)和不可共享的外在狀態(Extrinsic State),透過共享內在狀態來減少記憶體使用量。
ℹ️GoF 分類
Flyweight 屬於結構型模式(Structural Pattern),重點在於如何組合類別和物件以形成更大的結構。
什麼時候用?
- 系統需要建立大量相似物件(數千甚至數百萬個)
- 物件的大部分狀態可以被外部化
- 移除外在狀態後,許多物件可以被少數共享物件取代
- 應用程式不依賴物件的身份識別(共享物件不能用
==判斷)
什麼時候不該用?
⚠️過度設計警告
如果物件數量不多(幾十個),或者每個物件的狀態差異很大(幾乎沒有可共享的部分),Flyweight 只會增加複雜度而看不到記憶體節省。先用 Profiler 確認記憶體真的是瓶頸再考慮使用。
執行流程
分析物件狀態
區分哪些是內在狀態(可共享),哪些是外在狀態(每個不同)
定義 Flyweight
只包含內在狀態的共享物件
建立 Flyweight Factory
用 Pool/Cache 管理共享物件,已存在就返回,不存在才建立
外在狀態由 Client 傳入
使用 Flyweight 時,Client 把外在狀態作為參數傳給方法
大量使用
數千個邏輯物件實際上只引用少數幾個 Flyweight 實例
流程解讀:Flyweight 的第一步也是最關鍵的一步是「狀態分析」。把每個欄位歸類為 Intrinsic State(內在狀態,所有使用者共同的,如粒子的顏色和材質)或 Extrinsic State(外在狀態,每個使用者獨有的,如粒子的位置和速度)。Intrinsic State 存在共享的 Flyweight 物件中,Extrinsic State 存在 Context 物件中或在每次呼叫時作為參數傳入。Factory 用 Dictionary/Map 作為物件池,確保同一種 Flyweight 只建立一次。
程式碼範例
C# 版本
// Flyweight:共享的粒子類型(內在狀態)
public class ParticleType
{
public string Name { get; }
public string Color { get; }
public string Sprite { get; }
public ParticleType(string name, string color, string sprite)
{
Name = name; Color = color; Sprite = sprite;
}
public string Render(int x, int y, int speed)
=> $"[{Name}] color={Color} at ({x},{y}) speed={speed}";
}
// Flyweight Factory
public class ParticleTypeFactory
{
private readonly Dictionary<string, ParticleType> _cache = new();
public ParticleType GetType(string name, string color, string sprite)
{
var key = $"{name}_{color}";
if (!_cache.ContainsKey(key))
_cache[key] = new ParticleType(name, color, sprite);
return _cache[key];
}
public int TypeCount => _cache.Count;
}
// 粒子(外在狀態 + Flyweight 引用)
public class Particle
{
public ParticleType Type { get; }
public int X { get; set; }
public int Y { get; set; }
public int Speed { get; set; }
public Particle(ParticleType type, int x, int y, int speed)
{
Type = type; X = x; Y = y; Speed = speed;
}
public string Render() => Type.Render(X, Y, Speed);
}
// 使用
var factory = new ParticleTypeFactory();
var particles = new List<Particle>();
var random = new Random();
for (int i = 0; i < 10000; i++)
{
var type = factory.GetType("spark", "orange", "spark.png");
particles.Add(new Particle(type, random.Next(1920), random.Next(1080), random.Next(10)));
}
Console.WriteLine($"Particles: {particles.Count}, Shared types: {factory.TypeCount}");
// Particles: 10000, Shared types: 1TypeScript 版本
// Flyweight:共享的粒子類型
class ParticleType {
constructor(
public readonly name: string,
public readonly color: string,
public readonly sprite: string
) {}
render(x: number, y: number, speed: number): string {
return `[${this.name}] color=${this.color} at (${x},${y}) speed=${speed}`;
}
}
// Flyweight Factory
class ParticleTypeFactory {
private cache = new Map<string, ParticleType>();
getType(name: string, color: string, sprite: string): ParticleType {
const key = `${name}_${color}`;
if (!this.cache.has(key)) {
this.cache.set(key, new ParticleType(name, color, sprite));
}
return this.cache.get(key)!;
}
get typeCount(): number {
return this.cache.size;
}
}
// 粒子(外在狀態)
class Particle {
constructor(
public type: ParticleType,
public x: number,
public y: number,
public speed: number
) {}
render(): string {
return this.type.render(this.x, this.y, this.speed);
}
}
// 使用
const factory = new ParticleTypeFactory();
const particles: Particle[] = [];
for (let i = 0; i < 10000; i++) {
const type = factory.getType("spark", "orange", "spark.png");
particles.push(new Particle(type,
Math.floor(Math.random() * 1920),
Math.floor(Math.random() * 1080),
Math.floor(Math.random() * 10)
));
}
console.log(`Particles: ${particles.length}, Shared types: ${factory.typeCount}`);Python 版本
import random
# Flyweight:共享的粒子類型
class ParticleType:
def __init__(self, name: str, color: str, sprite: str):
self.name = name
self.color = color
self.sprite = sprite
def render(self, x: int, y: int, speed: int) -> str:
return f"[{self.name}] color={self.color} at ({x},{y}) speed={speed}"
# Flyweight Factory
class ParticleTypeFactory:
def __init__(self):
self._cache: dict[str, ParticleType] = {}
def get_type(self, name: str, color: str, sprite: str) -> ParticleType:
key = f"{name}_{color}"
if key not in self._cache:
self._cache[key] = ParticleType(name, color, sprite)
return self._cache[key]
@property
def type_count(self) -> int:
return len(self._cache)
# 粒子(外在狀態)
class Particle:
def __init__(self, particle_type: ParticleType, x: int, y: int, speed: int):
self.type = particle_type
self.x = x
self.y = y
self.speed = speed
def render(self) -> str:
return self.type.render(self.x, self.y, self.speed)
# 使用
factory = ParticleTypeFactory()
particles = []
for _ in range(10000):
ptype = factory.get_type("spark", "orange", "spark.png")
particles.append(Particle(ptype,
random.randint(0, 1920), random.randint(0, 1080), random.randint(0, 10)))
print(f"Particles: {len(particles)}, Shared types: {factory.type_count}")Java 版本
import java.util.*;
// Flyweight:共享的粒子類型
public class ParticleType {
private final String name;
private final String color;
private final String sprite;
public ParticleType(String name, String color, String sprite) {
this.name = name; this.color = color; this.sprite = sprite;
}
public String render(int x, int y, int speed) {
return String.format("[%s] color=%s at (%d,%d) speed=%d",
name, color, x, y, speed);
}
}
// Flyweight Factory
public class ParticleTypeFactory {
private Map<String, ParticleType> cache = new HashMap<>();
public ParticleType getType(String name, String color, String sprite) {
String key = name + "_" + color;
return cache.computeIfAbsent(key,
k -> new ParticleType(name, color, sprite));
}
public int getTypeCount() { return cache.size(); }
}
// 粒子(外在狀態)
public class Particle {
private ParticleType type;
private int x, y, speed;
public Particle(ParticleType type, int x, int y, int speed) {
this.type = type; this.x = x; this.y = y; this.speed = speed;
}
public String render() { return type.render(x, y, speed); }
}
// 使用
ParticleTypeFactory factory = new ParticleTypeFactory();
List<Particle> particles = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < 10000; i++) {
ParticleType type = factory.getType("spark", "orange", "spark.png");
particles.add(new Particle(type,
random.nextInt(1920), random.nextInt(1080), random.nextInt(10)));
}
System.out.printf("Particles: %d, Shared types: %d%n",
particles.size(), factory.getTypeCount());結構圖
結構解讀:Client 向 FlyweightFactory 請求 Flyweight,Factory 檢查快取中是否已存在:有就直接回傳,沒有才建立新的。Context(Particle)持有 Flyweight 的共享參考,同時保存自己獨有的外在狀態(位置、速度)。一萬個 Particle 可以共用同一個 ParticleType 實例,大幅節省記憶體。
實戰補充
💡資深開發者經驗
Flyweight 在你身邊無處不在,只是通常被包裝得看不出來。.NET 的 String Interning(string.Intern())就是 Flyweight — 相同的字串字面值只在記憶體中存一份。遊戲開發中的粒子系統、地圖磚塊(Tilemap)是經典場景:一萬個粒子共享同一組材質。在 Web 開發中,CSS class 也是一種 Flyweight 思維 — 定義一次,多處引用。使用 Flyweight 時要注意執行緒安全:共享物件如果被修改,會影響所有引用者,通常 Flyweight 物件設計成 Immutable。
理解測驗
🤔 Flyweight Pattern 把物件狀態分為哪兩種?
🤔 在粒子系統範例中,10000 個粒子實際上共享了幾個 ParticleType 物件?
🤔 以下哪個不是 Flyweight Pattern 的使用前提?
面試常見問題
Q: 如何判斷一個物件的狀態是 Intrinsic 還是 Extrinsic?
A: 問兩個問題:(1) 這個狀態對所有使用者都一樣嗎?一樣 → Intrinsic(如粒子的顏色、材質)。(2) 這個狀態每個使用者不同嗎?不同 → Extrinsic(如粒子的位置、速度)。Intrinsic State 放在 Flyweight 裡共享,Extrinsic State 由 Client 在每次呼叫時傳入。
Q: Flyweight 和 Object Pool 有什麼差別?
A: Flyweight 是共享 — 多個 Client 同時引用同一個物件。Object Pool 是借用 — Client 從池中借出物件、用完歸還,同一時間只有一個 Client 使用。Flyweight 適合唯讀共享物件,Object Pool 適合建立成本高且需要重複使用的可變物件(如資料庫連線)。
相關模式
| 模式 | 關係 | |------|------| | Singleton | Flyweight Factory 通常是 Singleton,集中管理共享物件池 | | Composite | Composite 的 Leaf 節點可以用 Flyweight 實作,大幅減少記憶體(如文件中的字元物件) | | Strategy | Flyweight 物件可以作為共享的 Strategy — 無狀態的策略只需要一個實例 | | Factory Method | Flyweight Factory 內部用 Factory Method 建立新的 Flyweight 實例 |
重點整理
💡一句話記住
Flyweight Pattern = 圖書館模式:一本書大家共讀,不用人手一本。 口訣:「共同的共享,不同的外傳」
| 概念 | 說明 | |------|------| | Flyweight(享元) | 只包含內在狀態的共享物件 | | Intrinsic State | 可共享的、不隨使用者改變的狀態 | | Extrinsic State | 每個使用者不同的狀態,由外部傳入 | | Flyweight Factory | 管理共享物件池,確保同一種只建立一次 | | 核心好處 | 大幅減少記憶體使用量 | | 代價 | 增加程式碼複雜度,需要區分兩種狀態 |