Flyweight Pattern(享元模式)

是什麼?

Flyweight Pattern 運用共享技術來有效支援大量細粒度的物件。它將物件的狀態分為可共享的內在狀態(Intrinsic State)和不可共享的外在狀態(Extrinsic State),透過共享內在狀態來減少記憶體使用量。

ℹ️GoF 分類

Flyweight 屬於結構型模式(Structural Pattern),重點在於如何組合類別和物件以形成更大的結構。

什麼時候用?

什麼時候不該用?

⚠️過度設計警告

如果物件數量不多(幾十個),或者每個物件的狀態差異很大(幾乎沒有可共享的部分),Flyweight 只會增加複雜度而看不到記憶體節省。先用 Profiler 確認記憶體真的是瓶頸再考慮使用。

執行流程

1

分析物件狀態

區分哪些是內在狀態(可共享),哪些是外在狀態(每個不同)

2

定義 Flyweight

只包含內在狀態的共享物件

3

建立 Flyweight Factory

用 Pool/Cache 管理共享物件,已存在就返回,不存在才建立

4

外在狀態由 Client 傳入

使用 Flyweight 時,Client 把外在狀態作為參數傳給方法

5

大量使用

數千個邏輯物件實際上只引用少數幾個 Flyweight 實例

流程解讀:Flyweight 的第一步也是最關鍵的一步是「狀態分析」。把每個欄位歸類為 Intrinsic State(內在狀態,所有使用者共同的,如粒子的顏色和材質)或 Extrinsic State(外在狀態,每個使用者獨有的,如粒子的位置和速度)。Intrinsic State 存在共享的 Flyweight 物件中,Extrinsic State 存在 Context 物件中或在每次呼叫時作為參數傳入。Factory 用 Dictionary/Map 作為物件池,確保同一種 Flyweight 只建立一次。

程式碼範例

C# 版本

csharp
// 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: 1

TypeScript 版本

typescript
// 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 版本

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 版本

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
requests flyweight
FlyweightFactory
creates / returns cached
Flyweight (ParticleType)
Context (Particle)

結構解讀:Client 向 FlyweightFactory 請求 Flyweight,Factory 檢查快取中是否已存在:有就直接回傳,沒有才建立新的。Context(Particle)持有 Flyweight 的共享參考,同時保存自己獨有的外在狀態(位置、速度)。一萬個 Particle 可以共用同一個 ParticleType 實例,大幅節省記憶體。

實戰補充

💡資深開發者經驗

Flyweight 在你身邊無處不在,只是通常被包裝得看不出來。.NET 的 String Interningstring.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 | 管理共享物件池,確保同一種只建立一次 | | 核心好處 | 大幅減少記憶體使用量 | | 代價 | 增加程式碼複雜度,需要區分兩種狀態 |

你可能也想看

Facade PatternProxy Pattern

按 ← → 鍵切換課程