15.2 解決方案
15.2.1 組合模式來解決
用來解決上述問題的一個合理的解決方案就是組合模式。那麼什麼是組合模式呢?
(1)組合模式定義
(2)應用組合模式來解決的思路
仔細分析上面不用模式的例子中,要區分組合對象和葉子對象的根本原因,就在於沒有把組合對象和葉子對象統一起來,也就是說,組合對象類型和葉子對象類型是完全不同的類型,這導致了操作的時候必須區分它們。
組合模式通過引入一個抽象的組件對象,作爲組合對象和葉子對象的父對象,這樣就把組合對象和葉子對象統一起來了,用戶使用的時候,始終是在操作組件對象,而不再去區分是在操作組合對象還是在操作葉子對象。
組合模式的關鍵就在於這個抽象類,這個抽象類既可以代表葉子對象,也可以代表組合對象,這樣用戶在操作的時候,對單個對象和組合對象的使用就具有了一致性。
15.2.2 模式結構和說明
組合模式的結構如圖15.1所示:
圖15.1 組合模式結構示意圖
Component:
抽象的組件對象,爲組合中的對象聲明接口,讓客戶端可以通過這個接口來訪問和管理整個對象結構,可以在裏面爲定義的功能提供缺省的實現。
Leaf:
葉子節點對象,定義和實現葉子對象的行爲,不再包含其它的子節點對象。
Composite:
組合對象,通常會存儲子組件,定義包含子組件的那些組件的行爲,並實現在組件接口中定義的與子組件有關的操作。
Client:
客戶端,通過組件接口來操作組合結構裏面的組件對象。
一種典型的Composite對象結構通常是如圖15.2所示的樹形結構,一個Composite對象可以包含多個葉子多象和其它的Composite對象,雖然15.2的圖看起來好像有些對稱,但是那只是爲了讓圖看起來美觀一點,並不是說Composite組合的對象結構就是這樣對稱的,這點要提前說明一下。
圖15.2 典型的Composite對象結構
15.2.3 組合模式的示例代碼
(1)先看看組件對象的定義,示例代碼如下:
/** * 抽象的組件對象,爲組合中的對象聲明接口,實現接口的缺省行爲 */ public abstract class Component { /** * 示意方法,子組件對象可能有的功能方法 */ public abstract void someOperation(); /** * 向組合對象中加入組件對象 * @param child 被加入組合對象中的組件對象 */ public void addChild(Component child) { // 缺省的實現,拋出例外,因爲葉子對象沒有這個功能, //或者子組件沒有實現這個功能 throw new UnsupportedOperationException( "對象不支持這個功能"); } /** * 從組合對象中移出某個組件對象 * @param child 被移出的組件對象 */ public void removeChild(Component child) { // 缺省的實現,拋出例外,因爲葉子對象沒有這個功能, //或者子組件沒有實現這個功能 throw new UnsupportedOperationException( "對象不支持這個功能"); } /** * 返回某個索引對應的組件對象 * @param index 需要獲取的組件對象的索引,索引從0開始 * @return 索引對應的組件對象 */ public Component getChildren(int index) { // 缺省的實現,拋出例外,因爲葉子對象沒有這個功能, //或者子組件沒有實現這個功能 throw new UnsupportedOperationException( "對象不支持這個功能"); } } |
(2)接下來看看Composite對象的定義,示例代碼如下:
/** * 組合對象,通常需要存儲子對象,定義有子部件的部件行爲, * 並實現在Component裏面定義的與子部件有關的操作 */ public class Composite extends Component { /** * 用來存儲組合對象中包含的子組件對象 */ private List<Component> childComponents = null; /** * 示意方法,通常在裏面需要實現遞歸的調用 */ public void someOperation() { if (childComponents != null){ for(Component c : childComponents){ //遞歸的進行子組件相應方法的調用 c.someOperation(); } } } public void addChild(Component child) { //延遲初始化 if (childComponents == null) { childComponents = new ArrayList<Component>(); } childComponents.add(child); } public void removeChild(Component child) { if (childComponents != null) { childComponents.remove(child); } } public Component getChildren(int index) { if (childComponents != null){ if(index>=0 && index<childComponents.size()){ return childComponents.get(index); } } return null; } } |
(3)該來看葉子對象的定義了,相對而言比較簡單,示例代碼如下:
/** * 葉子對象,葉子對象不再包含其它子對象 */ public class Leaf extends Component { /** * 示意方法,葉子對象可能有自己的功能方法 */ public void someOperation() { // do something } } |
(4)對於Client,就是使用Component接口來操作組合對象結構,由於使用方式千差萬別,這裏僅僅提供一個示範性質的使用,順便當作測試代碼使用,示例代碼如下:
public class Client { public static void main(String[] args) { //定義多個Composite對象 Component root = new Composite(); Component c1 = new Composite(); Component c2 = new Composite(); //定義多個葉子對象 Component leaf1 = new Leaf(); Component leaf2 = new Leaf(); Component leaf3 = new Leaf();
//組合成爲樹形的對象結構 root.addChild(c1); root.addChild(c2); root.addChild(leaf1); c1.addChild(leaf2); c2.addChild(leaf3);
//操作Component對象 Component o = root.getChildren(1); System.out.println(o); } } |
15.2.4 使用組合模式重寫示例
理解了組合模式的定義、結構和示例代碼過後,對組合模式應該有一定的掌握了,下面就來使用組合模式,來重寫前面不用模式的示例,看看用組合模式來實現會是什麼樣子,跟不用模式有什麼相同和不同之處。
爲了整體理解和把握整個示例,先來看看示例的整體結構,如圖15.3所示:
圖15.3 使用組合模式實現示例的結構示意圖
(1)首先就是要爲組合對象和葉子對象添加一個抽象的父對象做爲組件對象,在組件對象裏面,定義一個輸出組件本身名稱的方法以實現要求的功能,示例代碼如下:
/** * 抽象的組件對象 */ public abstract class Component { /** * 輸出組件自身的名稱 */ public abstract void printStruct(String preStr); /** * 向組合對象中加入組件對象 * @param child 被加入組合對象中的組件對象 */ public void addChild(Component child) { throw new UnsupportedOperationException( "對象不支持這個功能"); } /** * 從組合對象中移出某個組件對象 * @param child 被移出的組件對象 */ public void removeChild(Component child) { throw new UnsupportedOperationException( "對象不支持這個功能"); } /** * 返回某個索引對應的組件對象 * @param index 需要獲取的組件對象的索引,索引從0開始 * @return 索引對應的組件對象 */ public Component getChildren(int index) { throw new UnsupportedOperationException( "對象不支持這個功能"); } } |
(2)先看葉子對象的實現,它變化比較少,只是讓葉子對象繼承了組件對象,其它的跟不用模式比較,沒有什麼變化,示例代碼如下:
/** * 葉子對象 */ public class Leaf extends Component{ /** * 葉子對象的名字 */ private String name = ""; /** * 構造方法,傳入葉子對象的名字 * @param name 葉子對象的名字 */ public Leaf(String name){ this.name = name; } /** * 輸出葉子對象的結構,葉子對象沒有子對象,也就是輸出葉子對象的名字 * @param preStr 前綴,主要是按照層級拼接的空格,實現向後縮進 */ public void printStruct(String preStr){ System.out.println(preStr+"-"+name); } } |
(3)接下來看看組合對象的實現,這個對象變化就比較多,大致有如下的改變:
- 新的Composite對象需要繼承組件對象
- 原來用來記錄包含的其它組合對象的集合,和包含的其它葉子對象的集合,這兩個集合被合併成爲一個,就是統一的包含其它子組件對象的集合。使用組合模式來實現,不再需要區分到底是組合對象還是葉子對象了
- 原來的addComposite和addLeaf的方法,可以不需要了,合併實現成組件對象中定義的方法addChild,當然需要現在的Composite來實現這個方法。使用組合模式來實現,不再需要區分到底是組合對象還是葉子對象了
- 原來的printStruct方法的實現,完全要按照現在的方式來寫,變化較大
具體的示例代碼如下:
/** * 組合對象,可以包含其它組合對象或者葉子對象 */ public class Composite extends Component{ /** * 用來存儲組合對象中包含的子組件對象 */ private List<Component> childComponents = null; /** * 組合對象的名字 */ private String name = ""; /** * 構造方法,傳入組合對象的名字 * @param name 組合對象的名字 */ public Composite(String name){ this.name = name; }
public void addChild(Component child) { //延遲初始化 if (childComponents == null) { childComponents = new ArrayList<Component>(); } childComponents.add(child); } /** * 輸出組合對象自身的結構 * @param preStr 前綴,主要是按照層級拼接的空格,實現向後縮進 */ public void printStruct(String preStr){ //先把自己輸出去 System.out.println(preStr+"+"+this.name); //如果還包含有子組件,那麼就輸出這些子組件對象 if(this.childComponents!=null){ //然後添加一個空格,表示向後縮進一個空格 preStr+=" "; //輸出當前對象的子對象了 for(Component c : childComponents){ //遞歸輸出每個子對象 c.printStruct(preStr); } } } } |
(4)客戶端也有變化,客戶端不再需要區分組合對象和葉子對象了,統一都是使用組件對象,調用的方法也都要改變成組件對象定義的方法。示例代碼如下:
public class Client { public static void main(String[] args) { //定義所有的組合對象 Component root = new Composite("服裝"); Component c1 = new Composite("男裝"); Component c2 = new Composite("女裝");
//定義所有的葉子對象 Component leaf1 = new Leaf("襯衣"); Component leaf2 = new Leaf("夾克"); Component leaf3 = new Leaf("裙子"); Component leaf4 = new Leaf("套裝");
//按照樹的結構來組合組合對象和葉子對象 root.addChild(c1); root.addChild(c2); c1.addChild(leaf1); c1.addChild(leaf2); c2.addChild(leaf3); c2.addChild(leaf4); //調用根對象的輸出功能來輸出整棵樹 root.printStruct(""); } } |
通過上面的示例,大家可以看出,通過使用組合模式,把一個“部分-整體”的層次結構表示成了對象樹的結構,這樣一來,客戶端就無需再區分操作的是組合對象還是葉子對象了,對於客戶端而言,操作的都是組件對象。