一、組合模式的定義
組合(Composite)模式的定義:有時又叫作部分-整體模式,它是一種將對象組合成樹狀的層次結構的模式,用來表示“部分-整體”的關系,使用戶對單個對象和組合對象具有一致的訪問性。這種類型的設計模式屬於結構型模式,它創建了對象組的樹形結構。
聚合與組合都是表示整體和個體的關聯關系,他們之間最大的區別在於子類被父類控制的程度。組合的父子類關系比聚合要強:
-
聚合:has-a關系,父類包含子類,子類可以獨立於父類存在
Class ==> Student:班級和學生是一種聚合關系。一個班級,可以有學生,也可以沒學生;反過來,當把班級解散,學生仍然存在;同時學生可以歸屬於多個班級(興趣班、課外班)。
-
組合:part-of關系,父類擁有子類,子類不能獨立於父類存在
Body ⇒ Cell:身體與細胞是一個組合關系。但我們創建一個身體,細胞隨之被創建;反過來,當我們銷毀一個身體,細胞隨之被銷毀。
二、組合模式優缺點
組合模式的主要優點有:
- 組合模式使得客戶端代碼可以一致地處理單個對象和組合對象,無須關心自己處理的是單個對象,還是組合對象,這簡化了客戶端代碼;
- 更容易在組合體內加入新的對象,客戶端不會因為加入了新的對象而更改源代碼,滿足“開閉原則”;
其主要缺點是:
- 設計較復雜,客戶端需要花更多時間理清類之間的層次關系;
- 不容易限制容器中的構件;
- 不容易用繼承的方法來增加構件的新功能;
三、組合模式的實現
組合模式的結構不是很復雜,下面對它的結構和實現進行分析。
組合模式包含以下主要角色。
- 抽象構件(Component)角色:它的主要作用是為樹葉構件和樹枝構件聲明公共接口,並實現它們的默認行為。在透明式的組合模式中抽象構件還聲明訪問和管理子類的接口;在安全式的組合模式中不聲明訪問和管理子類的接口,管理工作由樹枝構件完成。
- 樹葉構件(Leaf)角色:是組合中的葉節點對象,它沒有子節點,用於實現抽象構件角色中 聲明的公共接口。
- 樹枝構件(Composite)角色:是組合中的分支節點對象,它有子節點。它實現了抽象構件角色中聲明的接口,它的主要作用是存儲和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法。
組合模式分為透明式的組合模式和安全式的組合模式。
透明方式:在該方式中,由於抽象構件聲明了所有子類中的全部方法,所以客戶端無須區別樹葉對象和樹枝對象,對客戶端來說是透明的。但其缺點是:樹葉構件本來沒有 Add()、Remove() 及 GetChild() 方法,卻要實現它們(空實現或拋異常),這樣會帶來一些安全性問題。其結構圖如圖所示:

假如要訪問集合 c0={leaf1,{leaf2,leaf3}} 中的元素,其對應的樹狀圖如圖所示:

其代碼實現如下:
public class CompositePattern { public static void main(String[] args) { Component c0=new Composite(); Component c1=new Composite(); Component leaf1=new Leaf("1"); Component leaf2=new Leaf("2"); Component leaf3=new Leaf("3"); c0.add(leaf1); c0.add(c1); c1.add(leaf2); c1.add(leaf3); c0.operation(); } } //抽象構件 interface Component { void add(Component c); void remove(Component c); Component getChild(int i); void operation(); } //樹葉構件 class Leaf implements Component { private String name; public Leaf(String name) { this.name=name; } public void add(Component c){ } public void remove(Component c){ } public Component getChild(int i) { return null; } public void operation() { System.out.println("樹葉"+name+":被訪問!"); } } //樹枝構件 class Composite implements Component { private ArrayList<Component> children=new ArrayList<Component>(); public void add(Component c) { children.add(c); } public void remove(Component c) { children.remove(c); } public Component getChild(int i) { return children.get(i); } public void operation() { for(Object obj:children) { ((Component)obj).operation(); } } }
輸出結果如下:
樹葉1:被訪問!
樹葉2:被訪問!
樹葉3:被訪問!
安全方式:在該方式中,將管理子構件的方法移到樹枝構件中,抽象構件和樹葉構件沒有對子對象的管理方法,這樣就避免了上一種方式的安全性問題,但由於葉子和分支有不同的接口,客戶端在調用時要知道樹葉對象和樹枝對象的存在,所以失去了透明性。其結構圖如圖所示:

其代碼實現如下:
public class CompositeSafePattern { public static void main(String[] args) { CompositeSafe c0=new CompositeSafe(); CompositeSafe c1=new CompositeSafe(); ComponentSafe leaf1=new LeafSafe("1"); ComponentSafe leaf2=new LeafSafe("2"); ComponentSafe leaf3=new LeafSafe("3"); c0.add(leaf1); c0.add(c1); c1.add(leaf2); c1.add(leaf3); c0.operation(); } } //抽象構件 interface ComponentSafe { void operation(); } //樹葉構件 class LeafSafe implements ComponentSafe { private String name; public LeafSafe(String name) { this.name=name; } public void operation() { System.out.println("樹葉"+name+":被訪問!"); } } //樹枝構件 class CompositeSafe implements ComponentSafe { private ArrayList<ComponentSafe> children=new ArrayList<ComponentSafe>(); public void add(ComponentSafe c) { children.add(c); } public void remove(ComponentSafe c) { children.remove(c); } public ComponentSafe getChild(int i) { return children.get(i); } public void operation() { for(Object obj:children) { ((ComponentSafe)obj).operation(); } } }
四、組合模式實際運用
說明:假如李先生到韶關“天街e角”生活用品店購物,用 1 個紅色小袋子裝了 2 包婺源特產(單價 7.9 元)、1 張婺源地圖(單價 9.9 元);用 1 個白色小袋子裝了 2 包韶關香藉(單價 68 元)和 3 包韶關紅茶(單價 180 元);用 1 個中袋子裝了前面的紅色小袋子和 1 個景德鎮瓷器(單價 380 元);用 1 個大袋子裝了前面的中袋子、白色小袋子和 1 雙李寧牌運動鞋(單價 198 元)。
最后“大袋子”中的內容有:{1 雙李寧牌運動鞋(單價 198 元)、白色小袋子{2 包韶關香菇(單價 68 元)、3 包韶關紅茶(單價 180 元)}、中袋子{1 個景德鎮瓷器(單價 380 元)、紅色小袋子{2 包婺源特產(單價 7.9 元)、1 張婺源地圖(單價 9.9 元)}}},現在要求編程顯示李先生放在大袋子中的所有商品信息並計算要支付的總價。
代碼實現如下(安全方式寫法):
/** * 說明:假如李先生到韶關“天街e角”生活用品店購物,用 1 個紅色小袋子裝了 2 包婺源特產(單價 7.9 元)、1 張婺源地圖(單價 9.9 元); * 用 1 個白色小袋子裝了 2 包韶關香藉(單價 68 元)和 3 包韶關紅茶(單價 180 元);用 1 個中袋子裝了前面的紅色小袋子和 1 個景德鎮瓷器(單價 380 元); * 用 1 個大袋子裝了前面的中袋子、白色小袋子和 1 雙李寧牌運動鞋(單價 198 元)。 * * 最后“大袋子”中的內容有:{ 1 雙李寧牌運動鞋(單價 198 元)、
* 白色小袋子{ 2 包韶關香菇(單價 68 元)、3 包韶關紅茶(單價 180 元)}、 * 中袋子{ 1個景德鎮瓷器(單價 380 元)、
* 紅色小袋子{2 包婺源特產(單價 7.9 元)、1 張婺源地圖(單價 9.9 元)}}}, * 現在要求編程顯示李先生放在大袋子中的所有商品信息並計算要支付的總價。 */ public class CompositeApplying { public static void main(String[] args) { float s = 0; Bags BigBag, mediumBag, smallRedBag, smallWhiteBag; Goods sp; BigBag = new Bags("大袋子"); mediumBag = new Bags("中袋子"); smallRedBag = new Bags("紅色小袋子"); smallWhiteBag = new Bags("白色小袋子"); sp = new Goods("婺源特產", 2, 7.9f); smallRedBag.add(sp); sp = new Goods("婺源地圖", 1, 9.9f); smallRedBag.add(sp); sp = new Goods("韶關香菇", 2, 68); smallWhiteBag.add(sp); sp = new Goods("韶關紅茶", 3, 180); smallWhiteBag.add(sp); sp = new Goods("景德鎮瓷器", 1, 380); mediumBag.add(sp); mediumBag.add(smallRedBag); sp = new Goods("李寧牌運動鞋", 1, 198); BigBag.add(sp); BigBag.add(smallWhiteBag); BigBag.add(mediumBag); System.out.println("您選購的商品有:"); BigBag.show(); s = BigBag.calculation(); System.out.println("要支付的總價是:" + s + "元"); } } //抽象構件:物品 interface Articles { public float calculation(); //計算 public void show(); } //樹葉構件:商品 class Goods implements Articles { private String name; //名字 private int quantity; //數量 private float unitPrice; //單價 public Goods(String name, int quantity, float unitPrice) { this.name = name; this.quantity = quantity; this.unitPrice = unitPrice; } public float calculation() { return quantity * unitPrice; } public void show() { System.out.println(name + "(數量:" + quantity + ",單價:" + unitPrice + "元)"); } } //樹枝構件:袋子 class Bags implements Articles { private String name; //名字 private ArrayList<Articles> bags = new ArrayList<Articles>(); public Bags(String name) { this.name = name; } public void add(Articles c) { bags.add(c); } public void remove(Articles c) { bags.remove(c); } public Articles getChild(int i) { return bags.get(i); } public float calculation() { float s = 0; for (Object obj : bags) { s += ((Articles) obj).calculation(); } return s; } public void show() { for (Object obj : bags) { ((Articles) obj).show(); } } }
測試結果如下:
您選購的商品有: 李寧牌運動鞋(數量:1,單價:198.0元) 韶關香菇(數量:2,單價:68.0元) 韶關紅茶(數量:3,單價:180.0元) 景德鎮瓷器(數量:1,單價:380.0元) 婺源特產(數量:2,單價:7.9元) 婺源地圖(數量:1,單價:9.9元) 要支付的總價是:1279.7元
五、組合模式的應用場景
前面分析了組合模式的結構與特點,下面分析它適用的以下應用場景。
- 在需要表示一個對象整體與部分的層次結構的場合。
- 要求對用戶隱藏組合對象與單個對象的不同,用戶可以用統一的接口使用組合結構中的所有對象的場合。
六、組合模式的擴展
如果對前面介紹的組合模式中的樹葉節點和樹枝節點進行抽象,也就是說樹葉節點和樹枝節點還有子節點,這時組合模式就擴展成復雜的組合模式了,如 Java AWT/Swing 中的簡單組件 JTextComponent 有子類 JTextField、JTextArea,容器組件 Container 也有子類 Window、Panel。復雜的組合模式的結構圖如圖所示:

