在現實生活中,存在很多“部分-整體”的關係,例如,大學中的部門與學院、總公司中的部門與分公司、學習用品中的書與書包、生活用品中的衣服與衣櫃、以及廚房中的鍋碗瓢盆等。在軟件開發中也是這樣,例如,文件系統中的文件與文件夾、窗體程序中的簡單控件與容器控件等。對這些簡單對象與複合對象的處理,如果用組合模式來實現會很方便。
組合模式的定義與特點
組合(Composite Pattern)模式的定義:有時又叫作整體-部分(Part-Whole)模式,它是通過將單個對象(葉子節點)和組合對象(樹枝節點)用相同的接口進行表示,使得客戶對單個對象和組合對象的使用具有一致性,屬於結構型設計模式。
組合模式一般用來描述整體與部分的關係,它將對象組織到樹形結構中,頂層的節點被稱爲根節點,根節點下面可以包含樹枝節點和葉子節點,樹枝節點下面又可以包含樹枝節點和葉子節點,樹形結構圖如下。
由上圖可以看出,其實根節點和樹枝節點本質上屬於同一種數據類型,可以作爲容器使用;而葉子節點與樹枝節點在語義上不屬於用一種類型。但是在組合模式中,會把樹枝節點和葉子節點看作屬於同一種數據類型(用統一接口定義),讓它們具備一致行爲。
這樣,在組合模式中,整個樹形結構中的對象都屬於同一種類型,帶來的好處就是用戶不需要辨別是樹枝節點還是葉子節點,可以直接進行操作,給用戶的使用帶來極大的便利。
組合模式的主要優點有:
- 組合模式使得客戶端代碼可以一致地處理單個對象和組合對象,無須關心自己處理的是單個對象,還是組合對象,這簡化了客戶端代碼;
- 更容易在組合體內加入新的對象,客戶端不會因爲加入了新的對象而更改源代碼,滿足“開閉原則”;
其主要缺點是:
- 設計較複雜,客戶端需要花更多時間理清類之間的層次關係;
- 不容易限制容器中的構件;
- 不容易用繼承的方法來增加構件的新功能;
- 使用組合模式時,其葉子和樹枝的聲明都是實現類,而不是接口,違反了依賴倒轉原則
使用場景
- 維護和展示部分-整體關係的場景(如樹形菜單、文件和文件夾管理)
- 從一個整體中能夠獨立出部分模塊或功能的場景
. 應用實例
- Swing中,Button、Checkbox等組件都是樹葉,而Container容器是樹枝
- 文本編輯時,可以單個字編輯,也可以整段編輯,還可以全文編輯
- 文件複製時,可以一個一個文件複製,也可以整個文件夾複製
組合模式的結構與實現
組合模式的結構不是很複雜,下面對它的結構和實現進行分析。
1. 模式的結構
組合模式包含以下主要角色。
- 抽象構件(Component)角色:它的主要作用是爲樹葉構件和樹枝構件聲明公共接口,並實現它們的默認行爲。在透明式的組合模式中抽象構件還聲明訪問和管理子類的接口;在安全式的組合模式中不聲明訪問和管理子類的接口,管理工作由樹枝構件完成。(總的抽象類或接口,定義一些通用的方法,比如新增、刪除)
- 樹葉構件(Leaf)角色:是組合中的葉節點對象,它沒有子節點,用於繼承或實現抽象構件。
- 樹枝構件(Composite)角色 / 中間構件:是組合中的分支節點對象,它有子節點,用於繼承和實現抽象構件。它的主要作用是存儲和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法。
組合模式分爲透明式的組合模式和安全式的組合模式。
(1) 透明方式
在該方式中,由於抽象構件聲明瞭所有子類中的全部方法,所以客戶端無須區別樹葉對象和樹枝對象,對客戶端來說是透明的。但其缺點是:樹葉構件本來沒有 Add()、Remove() 及 GetChild() 方法,卻要實現它們(空實現或拋異常),這樣會帶來一些安全性問題。其結構圖如圖 1 所示。
(2) 安全方式
在該方式中,將管理子構件的方法移到樹枝構件中,抽象構件和樹葉構件沒有對子對象的管理方法,這樣就避免了上一種方式的安全性問題,但由於葉子和分支有不同的接口,客戶端在調用時要知道樹葉對象和樹枝對象的存在,所以失去了透明性。其結構圖如圖 2 所示。
2. 模式的實現
假如要訪問集合 c0={leaf1,{leaf2,leaf3}} 中的元素,其對應的樹狀圖如圖 3 所示。
透明組合模式
下面爲透明式的組合模式的實現代碼。
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 {
public void add(Component c);
public void remove(Component c);
public Component getChild(int i);
public 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:被訪問!
安全組合模式
安全式的組合模式與透明式組合模式的實現代碼類似,只要對其做簡單修改就可以了,代碼如下。
首先修改 Component 代碼,只保留層次的公共行爲。
interface Component {
public void operation();
}
然後修改客戶端代碼,將樹枝構件類型更改爲 Composite 類型,以便獲取管理子類操作的方法。
public class CompositePattern {
public static void main(String[] args) {
Composite c0 = new Composite();
Composite 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();
}
}
組合模式的應用實例
【例1】用組合模式實現當用戶在商店購物後,顯示其所選商品信息,並計算所選商品總價的功能。
說明:假如李先生到韶關“天街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 元)}}},現在要求編程顯示李先生放在大袋子中的所有商品信息並計算要支付的總價。
本實例可按安全組合模式設計,其結構圖如圖 4 所示。
程序代碼如下:
package composite;
import java.util.ArrayList;
public class ShoppingTest {
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。複雜的組合模式的結構圖如圖 5 所示。
進階閱讀
如果您想深入瞭解組合模式,可猛擊閱讀以下文章。