意圖(Intent)#

透過共享來有效率地支援大量細粒度(fine-grained)物件。

動機(Motivation)#

某些應用程式在設計上適合大量使用物件,但直接為每個概念都建立獨立物件會耗費太多記憶體。例如,文件編輯器若為每個字元都建立一個物件,可以讓字元和嵌入元素被統一處理,也方便擴展新的字元集。但即使是中等大小的文件也可能需要數十萬個字元物件,記憶體成本不可接受。

Flyweight 是一個可在多個 context 中同時共享的物件。關鍵概念是區分兩種狀態:

  • Intrinsic state(內在狀態):儲存在 flyweight 中,與 context 無關,可共享。例如字元碼
  • Extrinsic state(外在狀態):取決於 context,不可共享,由客戶端在需要時傳入。例如字元在文件中的位置和字型

Flyweight 的核心設計決策:將物件狀態拆分為 intrinsic 和 extrinsic 兩部分。Intrinsic state 存在共享物件中;extrinsic state 由客戶端在每次操作時傳入。這是空間與時間的取捨——節省記憶體,但增加了傳遞和計算外在狀態的執行期成本。

以字母表為例,一個使用相同字型和顏色的文件,無論長度多長,只需要大約 100 個字元物件(約 ASCII 字元集的大小)。即使使用不同的字型-顏色組合,數量也不會大幅成長。

適用場景(Applicability)#

Flyweight 模式在以下條件全部成立時適用:

  • 應用程式使用大量物件
  • 儲存成本因物件數量過多而過高
  • 大部分物件狀態可以外在化
  • 移除外在狀態後,許多物件群組可以用相對少量的共享物件取代
  • 應用程式不依賴物件身分(identity)——因為 flyweight 物件可能被共享,身分測試會對概念上不同的物件回傳 true

結構(Structure)#

FlyweightFactory 建立和管理 flyweight 物件,確保正確共享。客戶端透過 factory 取得 flyweight,不直接實例化。客戶端負責計算或儲存 extrinsic state,在呼叫 flyweight 操作時傳入。

classDiagram
    class Flyweight {
        <<interface>>
        +Operation(extrinsicState)
    }
    class ConcreteFlyweight {
        -intrinsicState
        +Operation(extrinsicState)
    }
    class UnsharedConcreteFlyweight {
        -allState
        +Operation(extrinsicState)
    }
    class FlyweightFactory {
        -flyweights
        +GetFlyweight(key)
    }
    class Client
    Flyweight <|.. ConcreteFlyweight
    Flyweight <|.. UnsharedConcreteFlyweight
    FlyweightFactory o--> Flyweight
    Client --> FlyweightFactory
    Client --> ConcreteFlyweight
    Client --> UnsharedConcreteFlyweight

參與者(Participants)#

參與者範例職責
FlyweightGlyph宣告介面,flyweight 透過此介面接收並作用於 extrinsic state
ConcreteFlyweightCharacter實作 Flyweight 介面,儲存 intrinsic state。必須是可共享的,所有儲存的狀態必須與 context 無關
UnsharedConcreteFlyweightRow、Column不是所有 Flyweight 子類別都需要共享。Flyweight 介面「允許」共享但不「強制」共享。UnsharedConcreteFlyweight 常以 ConcreteFlyweight 作為子元件
FlyweightFactory-建立和管理 flyweight 物件,確保共享。客戶端請求 flyweight 時,提供既有實例或建立新的
Client-持有 flyweight 引用,計算或儲存 flyweight 的 extrinsic state

協作方式(Collaborations)#

  • flyweight 運作所需的狀態必須區分為 intrinsic 或 extrinsic。Intrinsic state 儲存在 ConcreteFlyweight 中;extrinsic state 由 Client 儲存或計算,在呼叫操作時傳入
  • Client 不應直接實例化 ConcreteFlyweight,必須透過 FlyweightFactory 取得以確保正確共享

優缺點(Consequences)#

  • Flyweight 可能引入與傳遞、查找、計算 extrinsic state 相關的執行期成本,但這些成本被記憶體節省所抵消
  • 記憶體節省取決於:共享帶來的實例數量減少、每個物件的 intrinsic state 量、extrinsic state 是計算的還是儲存的
  • 最大節省發生在:物件有大量 intrinsic 和 extrinsic state,且 extrinsic state 可以用計算取代儲存
  • 常與 Composite 搭配:共享的葉節點無法儲存父引用,父引用必須作為 extrinsic state 傳入

使用 Flyweight 時,共享的物件不能儲存父引用。若與 Composite 模式結合,父引用必須作為 extrinsic state 傳入,這會顯著影響階層結構中物件的溝通方式。

實作要點(Implementation)#

  • 移除 extrinsic state:模式能否適用,主要取決於能否容易地辨識並移除 extrinsic state。若 extrinsic state 的種類和物件數量一樣多,共享就沒有意義。理想情況是 extrinsic state 可以從一個儲存需求更小的獨立結構中計算得出
  • 管理共享物件:FlyweightFactory 通常使用關聯式儲存(如 hash table)讓客戶端查找 flyweight。是否需要 reference counting 或 GC 取決於 flyweight 的數量——若數量固定且少(如 ASCII 字元集),可以永久保留不回收

已知應用(Known Uses)#

  • InterViews Doc 編輯器:每個字元用 Glyph flyweight 表示,intrinsic state 為字元碼和樣式索引,extrinsic state 僅有位置。一份 180,000 個字元的文件只需配置 480 個字元物件
  • ET++:用 flyweight 支援 look-and-feel 獨立性。每個 widget 有對應的 Layout flyweight(如 ScrollbarLayout、MenubarLayout),由 Look 抽象工廠建立。Layout 物件本質上是實作為 flyweight 的 Strategy 物件

相關模式(Related Patterns)#

  • Composite:常與 Flyweight 結合,用有向無環圖(DAG)搭配共享葉節點來實作邏輯上的階層結構
  • StateStrategy:這兩個模式的物件常實作為 flyweight