意圖(Intent)#
將物件組合成樹狀結構以表達「部分-整體」的階層關係。Composite 讓客戶端能以一致的方式對待個別物件和物件組合。
動機(Motivation)#
繪圖應用程式讓使用者用簡單元件組合出複雜圖形,還能將元件群組化形成更大的元件,再進一步群組化。簡單的實作會為基本圖形和容器定義不同的類別,但問題是:客戶端程式碼必須區分處理基本物件和容器物件,即使大多數時候使用者是一視同仁地對待它們。
Composite 模式的關鍵是一個抽象類別(如 Graphic),同時代表基本元素和它們的容器。Graphic 宣告繪圖操作(如 Draw),也宣告管理子元件的操作。
- Leaf 類別(Line、Rectangle、Text):實作基本圖形,不含子元件
- Composite 類別(Picture):包含一組 Graphic 子元件,
Draw操作會遞迴呼叫子元件的Draw
因為 Picture 的介面也符合 Graphic 介面,所以 Picture 可以遞迴地組合其他 Picture。
Composite 的核心價值:客戶端程式碼不需要知道它處理的是 Leaf 還是 Composite,消除了型別判斷和條件分支的需求,大幅簡化客戶端邏輯。
適用場景(Applicability)#
- 想表達物件的「部分-整體」階層結構
- 希望客戶端能忽略組合物件與個別物件的差異,以統一方式處理所有物件
結構(Structure)#
Component 是共同介面,Leaf 代表沒有子元件的終端物件,Composite 儲存子元件並實作子元件相關的操作。客戶端透過 Component 介面操作,形成樹狀的遞迴結構。
classDiagram
class Component {
<<interface>>
+Operation()
+Add(Component)
+Remove(Component)
+GetChild(int)
}
class Leaf {
+Operation()
}
class Composite {
-children
+Operation()
+Add(Component)
+Remove(Component)
+GetChild(int)
}
class Client
Component <|.. Leaf
Component <|.. Composite
Composite o--> Component : children
Client --> Component參與者(Participants)#
| 參與者 | 範例 | 職責 |
|---|---|---|
| Component | Graphic | 宣告組合中物件的共同介面;實作所有類別共有的預設行為;宣告存取和管理子元件的介面;(選用)定義存取父元件的介面 |
| Leaf | Rectangle、Line、Text 等 | 代表組合中的葉節點,沒有子元件;定義基本物件的行為 |
| Composite | Picture | 定義有子元件的元件行為;儲存子元件;實作 Component 介面中子元件相關的操作 |
| Client | - | 透過 Component 介面操作組合結構中的物件 |
協作方式(Collaborations)#
- 客戶端透過 Component 介面與組合結構互動。若接收者是 Leaf,直接處理請求;若接收者是 Composite,將請求轉發給子元件,可能在轉發前後執行額外操作
優缺點(Consequences)#
- 定義遞迴的類別階層:基本物件可以組合成更複雜的物件,再繼續組合,無限遞迴
- 簡化客戶端:客戶端統一處理 Leaf 和 Composite,不需要判斷類型
- 容易新增元件:新的 Leaf 或 Composite 子類別自動與既有結構和客戶端程式碼相容
- 可能導致設計過度泛化:難以限制 Composite 只包含特定類型的子元件,需依賴執行期檢查而非型別系統
實作要點(Implementation)#
- 父元件引用:在 Component 中維護父元件引用,便於向上遍歷和刪除。新增/移除子元件時必須同步更新父引用以維持一致性
- 共享元件:可透過 Flyweight 模式實現,但共享時子元件無法儲存父引用
- 最大化 Component 介面:盡量在 Component 中定義共同操作。例如將 Leaf 視為「永遠沒有子元件的 Component」,在 Component 中提供預設的子元件存取操作
- 子元件管理操作的宣告位置——安全性 vs. 透明性的取捨:
- 宣告在 Component:透明(統一介面),但不安全(可能對 Leaf 執行無意義操作)
- 宣告在 Composite:安全(編譯期檢查),但不透明(需要型別轉換)
- 書中傾向透明性優先,在 Component 中提供預設的
Add/Remove,失敗時丟出例外
將子元件管理操作(Add/Remove)放在 Component 層級雖然提升透明性,但對 Leaf 呼叫
Add可能代表程式邏輯錯誤。建議讓預設的Add/Remove在不允許子元件時拋出例外,而非靜默忽略。
- 子元件排序:若需要維護子元件順序(如前後排列、語法樹),需仔細設計存取介面,可搭配 Iterator 模式
- 快取:Composite 可快取子元件的聚合資訊(如 bounding box),但子元件變更時需失效快取,搭配父引用效果最佳
- 元件的刪除責任:在無 GC 的語言中,通常由 Composite 負責刪除其子元件
- 儲存結構的選擇:可使用鏈結串列、陣列、雜湊表等,視效能需求而定
已知應用(Known Uses)#
- Smalltalk MVC:原始 View 類別就是 Composite,後來 Smalltalk-80 4.0 拆分為 VisualComponent、View 和 CompositeView
- ET++:VObjects 階層
- InterViews:Styles、Graphics、Glyphs
- RTL Smalltalk 編譯器:RTLExpression 構成語法樹的 Composite 結構
- 金融領域:投資組合(portfolio)作為個別資產的 Composite
- Command 模式:MacroCommand 就是 Command 的 Composite
相關模式(Related Patterns)#
- Chain of Responsibility:常利用 component-parent 連結
- Decorator:常與 Composite 搭配使用,兩者通常共享相同的父類別
- Flyweight:可用來共享元件,但共享的元件無法維護父引用
- Iterator:用來遍歷 Composite 結構
- Visitor:將分散在 Composite 和 Leaf 中的操作集中化