意圖(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)#
| 參與者 | 範例 | 職責 |
|---|---|---|
| Flyweight | Glyph | 宣告介面,flyweight 透過此介面接收並作用於 extrinsic state |
| ConcreteFlyweight | Character | 實作 Flyweight 介面,儲存 intrinsic state。必須是可共享的,所有儲存的狀態必須與 context 無關 |
| UnsharedConcreteFlyweight | Row、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)搭配共享葉節點來實作邏輯上的階層結構
- State 和 Strategy:這兩個模式的物件常實作為 flyweight