【白話設計模式十七】組合模式(Composite)

#0 系列目錄#java

#1 場景問題# ##1.1 商品類別樹## 考慮這樣一個實際的應用:管理商品類別樹。算法

在實現跟商品有關的應用系統的時候,一個很常見的功能就是商品類別樹的管理,好比有以下所示的商品類別樹:設計模式

- 服裝
    - 男裝
        - 襯衣
        - 夾克
    - 女裝
        - 裙子
        - 套裝

仔細觀察上面的商品類別樹,有如下幾個明顯的特色:緩存

有一個根節點,好比服裝,它沒有父節點,它能夠包含其它的節點;安全

樹枝節點,有一類節點能夠包含其它的節點,稱之爲樹枝節點,好比男裝、女裝;測試

葉子節點,有一類節點沒有子節點,稱之爲葉子節點,好比襯衣、夾克、裙子、套裝;ui

如今須要管理商品類別樹,假如就要求能實現輸出如上商品類別樹的結構的功能,應該如何實現呢?this

##1.2 不用模式的解決方案## 要管理商品類別樹,就是要管理樹的各個節點,如今樹上的節點有三類,根節點、樹枝節點和葉子節點,再進一步分析發現,根節點和樹枝節點是相似的,都是能夠包含其它節點的節點,把它們稱爲容器節點。.net

這樣一來,商品類別樹的節點就被分紅了兩種,一種是容器節點,另外一種是葉子節點。容器節點能夠包含其它的容器節點或者葉子節點。把它們分別實現成爲對象,也就是容器對象和葉子對象,容器對象能夠包含其它的容器對象或者葉子對象,換句話說,容器對象是一種組合對象。設計

而後在組合對象和葉子對象裏面去實現要求的功能就能夠了,看看代碼實現。

  1. 先看葉子對象的代碼實現,示例代碼以下:
/**
 * 葉子對象
 */
public class Leaf {
    /**
     * 葉子對象的名字
     */
    private String name = "";

    /**
     * 構造方法,傳入葉子對象的名字
     * @param name 葉子對象的名字
     */
    public Leaf(String name){
       this.name = name;
    }

    /**
     * 輸出葉子對象的結構,葉子對象沒有子對象,也就是輸出葉子對象的名字
     * @param preStr 前綴,主要是按照層級拼接的空格,實現向後縮進
     */
    public void printStruct(String preStr){
       System.out.println(preStr+"-"+name);
    }
}
  1. 再來看看組合對象的代碼實現,組合對象裏面能夠包含其它的組合對象或者是葉子對象,因爲類型不同,須要分開記錄。示例代碼以下:
/**
 * 組合對象,能夠包含其它組合對象或者葉子對象
 */
public class Composite {
    /**
     * 用來記錄包含的其它組合對象
     */
    private Collection<Composite> childComposite = new ArrayList<Composite>();
    /**
     * 用來記錄包含的其它葉子對象
     */
    private Collection<Leaf> childLeaf = new ArrayList<Leaf>();
    /**
     * 組合對象的名字
     */
    private String name = "";

    /**
     * 構造方法,傳入組合對象的名字
     * @param name 組合對象的名字
     */
    public Composite(String name){
       this.name = name;
    }

    /**
     * 向組合對象加入被它包含的其它組合對象
     * @param c 被它包含的其它組合對象
     */
    public void addComposite(Composite c){
       this.childComposite.add(c);
    }
    /**
     * 向組合對象加入被它包含的葉子對象
     * @param leaf 被它包含的葉子對象
     */
    public void addLeaf(Leaf leaf){
       this.childLeaf.add(leaf);
    }
    /**
     * 輸出組合對象自身的結構
     * @param preStr 前綴,主要是按照層級拼接的空格,實現向後縮進
     */
    public void printStruct(String preStr){
       //先把本身輸出去
       System.out.println(preStr+"+"+this.name);
       //而後添加一個空格,表示向後縮進一個空格,輸出本身包含的葉子對象
       preStr+=" ";
       for(Leaf leaf : childLeaf){
           leaf.printStruct(preStr);
       }
       //輸出當前對象的子對象了
       for(Composite c : childComposite){
           //遞歸輸出每一個子對象
           c.printStruct(preStr);
       }
    }
}
  1. 寫個客戶端來測試一下,看看是否能實現要求的功能,示例代碼以下:
public class Client {
    public static void main(String[] args) {
       //定義全部的組合對象
       Composite root = new Composite("服裝");
       Composite c1 = new Composite("男裝");
       Composite c2 = new Composite("女裝");

       //定義全部的葉子對象
       Leaf leaf1 = new Leaf("襯衣");
       Leaf leaf2 = new Leaf("夾克");
       Leaf leaf3 = new Leaf("裙子");
       Leaf leaf4 = new Leaf("套裝");

       //按照樹的結構來組合組合對象和葉子對象
       root.addComposite(c1);
       root.addComposite(c2);     
       c1.addLeaf(leaf1);
       c1.addLeaf(leaf2);      
       c2.addLeaf(leaf3);
       c2.addLeaf(leaf4);      

       //調用根對象的輸出功能來輸出整棵樹
       root.printStruct("");
    }
}

##1.3 有何問題## 上面的實現,雖然能實現要求的功能,可是有一個很明顯的問題:那就是必須區分組合對象和葉子對象,並進行有區別的對待,好比在Composite和Client裏面,都須要去區別對待這兩種對象。

區別對待組合對象和葉子對象,不只讓程序變得複雜,還對功能的擴展也帶來不便。實際上,大多數狀況下用戶並不想要去區別它們,而是認爲它們是同樣的,這樣他們操做起來最簡單。

換句話說,對於這種具備總體與部分關係,並能組合成樹形結構的對象結構,如何纔可以以一個統一的方式來進行操做呢?

#2 解決方案# ##2.1 組合模式來解決## 用來解決上述問題的一個合理的解決方案就是組合模式。那麼什麼是組合模式呢?

  1. 組合模式定義

輸入圖片說明

  1. 應用組合模式來解決的思路

仔細分析上面不用模式的例子中,要區分組合對象和葉子對象的根本緣由,就在於沒有把組合對象和葉子對象統一塊兒來,也就是說,組合對象類型和葉子對象類型是徹底不一樣的類型,這致使了操做的時候必須區分它們。

組合模式經過引入一個抽象的組件對象,做爲組合對象和葉子對象的父對象,這樣就把組合對象和葉子對象統一塊兒來了,用戶使用的時候,始終是在操做組件對象,而再也不去區分是在操做組合對象仍是在操做葉子對象

組合模式的關鍵就在於這個抽象類,這個抽象類既能夠表明葉子對象,也能夠表明組合對象,這樣用戶在操做的時候,對單個對象和組合對象的使用就具備了一致性。

##2.2 模式結構和說明## 組合模式的結構如圖15.1所示:

輸入圖片說明

Component:抽象的組件對象,爲組合中的對象聲明接口,讓客戶端能夠經過這個接口來訪問和管理整個對象結構,能夠在裏面爲定義的功能提供缺省的實現。

Leaf:葉子節點對象,定義和實現葉子對象的行爲,再也不包含其它的子節點對象。

Composite:組合對象,一般會存儲子組件,定義包含子組件的那些組件的行爲,並實如今組件接口中定義的與子組件有關的操做。

Client:客戶端,經過組件接口來操做組合結構裏面的組件對象。

一種典型的Composite對象結構一般是如圖15.2所示的樹形結構,一個Composite對象能夠包含多個葉子多象和其它的Composite對象,雖然15.2的圖看起來好像有些對稱,可是那只是爲了讓圖看起來美觀一點,並非說Composite組合的對象結構就是這樣對稱的,這點要提早說明一下。

輸入圖片說明

##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("對象不支持這個功能");
    }
}
  1. 接下來看看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;
    }
}
  1. 該來看葉子對象的定義了,相對而言比較簡單,示例代碼以下:
/**
 * 葉子對象,葉子對象再也不包含其它子對象
 */
public class Leaf extends Component {
    /**
     * 示意方法,葉子對象可能有本身的功能方法
     */
    public void someOperation() {
       // do something
    }
}
  1. 對於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);
    }
}

##2.4 使用組合模式重寫示例## 理解了組合模式的定義、結構和示例代碼事後,對組合模式應該有必定的掌握了,下面就來使用組合模式,來重寫前面不用模式的示例,看看用組合模式來實現會是什麼樣子,跟不用模式有什麼相同和不一樣之處。

爲了總體理解和把握整個示例,先來看看示例的總體結構,如圖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("對象不支持這個功能");
    }
}
  1. 先看葉子對象的實現,它變化比較少,只是讓葉子對象繼承了組件對象,其它的跟不用模式比較,沒有什麼變化,示例代碼以下:
/**
 * 葉子對象
 */
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);
    }
}
  1. 接下來看看組合對象的實現,這個對象變化就比較多,大體有以下的改變:

新的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);
           }
       }
    }
}
  1. 客戶端也有變化,客戶端再也不須要區分組合對象和葉子對象了,統一都是使用組件對象,調用的方法也都要改變成組件對象定義的方法。示例代碼以下:
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("");
    }
}

經過上面的示例,你們能夠看出,經過使用組合模式,把一個「部分-總體」的層次結構表示成了對象樹的結構,這樣一來,客戶端就無需再區分操做的是組合對象仍是葉子對象了,對於客戶端而言,操做的都是組件對象

#3 模式講解# ##3.1 認識組合模式##

  1. 組合模式的目的

組合模式的目的是:讓客戶端再也不區分操做的是組合對象仍是葉子對象,而是以一個統一的方式來操做

實現這個目標的關鍵之處,是設計一個抽象的組件類,讓它能夠表明組合對象和葉子對象。這樣一來,客戶端就不用區分究竟是組合對象仍是葉子對象了,只須要所有當成組件對象進行統一的操做就能夠了。

  1. 對象樹

一般,組合模式會組合出樹形結構來,組成這個樹形結構所使用的多個組件對象,就天然的造成了對象樹

這也意味着凡是可使用對象樹來描述或操做的功能,均可以考慮使用組合模式,好比讀取XML文件,或是對語句進行語法解析等。

  1. 組合模式中的遞歸

組合模式中的遞歸,指的是對象遞歸組合,不是常說的遞歸算法。一般咱們談的遞歸算法,是指「一個方法會調用方法本身」這樣的算法,是從功能上來說的,好比那個經典的求階乘的例子,示例以下:

public class RecursiveTest {
    /**
     * 示意遞歸算法,求階乘。這裏只是簡單的實現,只能實現求數值較小的階乘,
     * 對於數據比較大的階乘,好比求100的階乘應該採用java.math.BigDecimal
     * 或是java.math.BigInteger
     * @param a 求階乘的數值
     * @return 該數值的階乘值
     */
    public int recursive(int a){
       if(a==1){
           return 1;
       }     
       return a * recursive(a-1);
    }  
    public static void main(String[] args) {
       RecursiveTest test = new RecursiveTest();
       int result = test.recursive(5);
       System.out.println("5的階乘="+result);
    }
}

而這裏的組合模式中的遞歸,是對象自己的遞歸,是對象的組合方式,是從設計上來說的,在設計上稱做遞歸關聯,是對象關聯關係的一種,若是用UML來表示對象的遞歸關聯的話,一對一的遞歸關聯如圖15.4所示,而一對多的遞歸關聯如圖15.5所示:

輸入圖片說明

另外組合對象還有一個特色,就是理論上沒有層次限制,組合對象A包含組合對象B,組合對象B又包含組合對象C……,這樣下去是沒有盡頭的。所以在實現的時候,一個必然的選擇就是遞歸實現

  1. Component中是否應該實現一個Component列表

大多數狀況下,一個Composite對象會持有子節點的集合。有些朋友可能就會想,那麼能不能把這個子節點集合定義到Component中去呢?由於在Component中還聲明瞭一些操做子節點的方法,這樣一來,大部分的工做就能夠在Component中完成了。

事實上,這種方法是不太好的,由於在父類來存放子類的實例對象,對於Composite節點來講沒有什麼,它原本就須要存放子節點,可是對於葉子節點來講,就會致使空間的浪費,由於葉節點自己不須要子節點

所以只有當組合結構中葉子對象數目較少的時候,才值得使用這種方法。

  1. 最大化Component定義

前面講到了組合模式的目的是:讓客戶端再也不區分操做的是組合對象仍是葉子對象,而是以一種統一的方式來操做

因爲要統一兩種對象的操做,因此Component裏面的方法也主要是兩種對象對外方法的和,換句話說,有點大雜燴的意思,組件裏面既有葉子對象須要的方法,也有組合對象須要的方法

其實這種實現是與類的設計原則相沖突的,類的設計有這樣的原則:一個父類應該只定義那些對它的子類有意義的操做。可是看看上面的實現就知道,Component中的有些方法對於葉子對象是沒有意義的。那麼怎麼解決這一衝突呢?

常見的作法是在Component裏面爲對某些子對象沒有意義的方法,提供默認的實現,或是默認拋出不支持該功能的例外。這樣一來,若是子對象須要這個功能,那就覆蓋實現它,若是不須要,那就不用管了,使用父類的默認實現就能夠了。

從另外一個層面來講,若是把葉子對象當作是一個特殊的Composite對象,也就是沒有子節點的組合對象而已。這樣看來,對於Component而言,子對象就所有看做是組合對象,所以定義的全部方法都是有意義的了

  1. 子部件排序

在某些應用中,使用組合模式的時候,須要按照必定的順序來使用子組件對象,好比進行語法分析的時候,使用組合模式構建的抽象語法樹,在解析執行的時候,是須要按照順序來執行的。

對於這樣的功能,須要在設計的時候,就要把組件對象的索引考慮進去,並仔細的設計對子節點的訪問和管理接口,一般的方式是須要按照順序來存儲,這樣在獲取的時候就能夠按照順序獲得了。能夠考慮結合Iterator模式來實現按照順序的訪問組件對象

##3.2 安全性和透明性## 根據前面的講述,在組合模式中,把組件對象分紅了兩種,一種是能夠包含子組件的Composite對象,一種是不能包含其它組件對象的葉子對象

Composite對象就像是一個容器,能夠包含其它的Composite對象或葉子對象。固然有了容器,就要能對這個容器進行維護,須要向裏面添加對象,並可以從容器裏面獲取對象,還有能從容器中刪除對象,也就是說須要管理子組件對象。

這就產生了一個很重要的問題:到底在組合模式的類層次結構中,在哪一些類裏面定義這些管理子組件的操做,到底應該在Component中聲明這些操做,仍是在Composite中聲明這些操做?

這就須要仔細思考,在不一樣的實現中,進行安全性和透明性的權衡選擇。

這裏所說的安全性是指從客戶使用組合模式上看是否更安全。若是是安全的,那麼不會有發生誤操做的可能,能訪問的方法都是被支持的功能。

這裏所說的透明性是指從客戶使用組合模式上,是否須要區分究竟是組合對象仍是葉子對象。若是是透明的,那就是再也不區分,對於客戶而言,都是組件對象,具體的類型對於客戶而言是透明的,是客戶無須要關心的。

  1. 透明性的實現

若是把管理子組件的操做定義在Component中,那麼客戶端只須要面對Component,而無需關心具體的組件類型,這種實現方式就是透明性的實現。事實上,前面示例的實現方式都是這種實現方式。

可是透明性的實現是以安全性爲代價的,由於在Component中定義的一些方法,對於葉子對象來講是沒有意義的,好比:增長、刪除子組件對象。而客戶不知道這些區別,對客戶是透明的,所以客戶可能會對葉子對象調用這種增長或刪除子組件的方法,這樣的操做是不安全的。

組合模式的透明性實現,一般的方式是:在Component中聲明管理子組件的操做,並在Component中爲這些方法提供缺省的實現,若是是有子對象不支持的功能,缺省的實現能夠是拋出一個例外,來表示不支持這個功能

  1. 安全性的實現

若是把管理子組件的操做定義在Composite中,那麼客戶在使用葉子對象的時候,就不會發生使用添加子組件或是刪除子組件的操做了,由於壓根就沒有這樣的功能,這種實現方式是安全的。

可是這樣一來,客戶端在使用的時候,就必須區分到底使用的是Composite對象,仍是葉子對象,不一樣對象的功能是不同的。也就是說,這種實現方式,對客戶而言就不是透明的了。

下面把用透明性方式實現的示例,改爲用安全性的方式再實現一次,這樣你們能夠對比來看,能夠更好的理解組合模式的透明性和安全性這兩種實現方式。

先仍是來看一下使用安全性方式實現示例的結構,如圖15.6所示:

輸入圖片說明

(1)首先看看Component的定義,跟透明性的實現相比,安全性的實現方式,Component裏面再也不定義管理和操做子組件的方法,把相應的方法都去掉。示例代碼以下:

/**
 * 抽象的組件對象,安全性的實現方式
 */
public abstract class Component {
    /**
     * 輸出組件自身的名稱
     */
    public abstract void printStruct(String preStr);
}

(2)Composite對象和Leaf對象的實現都沒有任何的變化,這裏就不去贅述了

(3)接下來看看客戶端的實現,客戶端的變化主要是要區分Composite對象和Leaf對象,而原來是不區分的,都是Component對象。示例代碼以下:

public class Client {
    public static void main(String[] args) {
       //定義全部的組合對象
       Composite root = new Composite("服裝");
       Composite c1 = new Composite("男裝");
       Composite c2 = new Composite("女裝");
       //定義全部的葉子對象
       Leaf leaf1 = new Leaf("襯衣");
       Leaf leaf2 = new Leaf("夾克");
       Leaf leaf3 = new Leaf("裙子");
       Leaf leaf4 = new Leaf("套裝");

       //按照樹的結構來組合組合對象和葉子對象
       root.addChild(c1);
       root.addChild(c2);      
       c1.addChild(leaf1);
       c1.addChild(leaf2);     
       c2.addChild(leaf3);
       c2.addChild(leaf4);
     
       //調用根對象的輸出功能來輸出整棵樹
       root.printStruct("");
    }
}

從上面的示例能夠看出,從實現上,透明性和安全性的實現差異並非很大。

  1. 兩種實現方式的選擇

對於組合模式而言,在安全性和透明性上,會更看重透明性,畢竟組合模式的功能就是要讓用戶對葉子對象和組合對象的使用具備一致性

並且對於安全性的實現,須要區分是組合對象仍是葉子對象,可是有的時候,你須要將對象進行類型轉換,卻發現類型信息丟失了,只好強行轉換,這種類型轉換必然是不夠安全的

對於這種狀況的處理方法是在Component裏面定義一個getComposite的方法,用來判斷是組合對象仍是葉子對象,若是是組合對象,就返回組合對象,若是是葉子對象,就返回null,這樣就能夠先判斷,而後再強制轉換。

所以在使用組合模式的時候,建議多用透明性的實現方式,而少用安全性的實現方式

##3.3 父組件引用## 在上面的示例中,都是在父組件對象裏面,保存有子組件的引用,也就是說都是從父到子的引用。而本節來討論一下子組件對象到父組件對象的引用,這個在實際開發中也是很是有用的,好比:

如今要刪除某個商品類別。若是這個類別沒有子類別的話,直接刪除就行了,沒有太大的問題,可是若是它還有子類別,這就涉及到它的子類別如何處理了,一種狀況是連帶所有刪除,一種是上移一層,把被刪除的商品類別對象的父商品類別,設置成爲被刪除的商品類別的子類別的父商品類別。

如今要進行商品類別的細化和調整,把本來屬於A類別的一些商品類別,調整到B類別裏面去,某個商品類別的調整會伴隨着它全部的子類別一塊兒調整。這樣的調整可能會:把本來是兄弟關係的商品類別變成父子關係,也可能會把本來是父子關係的商品類別調整成了兄弟關係,如此等等會有不少種可能。

要實現上述的功能,一個較爲簡單的方案就是在保持從父組件到子組件引用的基礎上,再增長保持從子組件到父組件的引用,這樣在刪除一個組件對象或是調整一個組件對象的時候,能夠經過調整父組件的引用來實現,這能夠大大簡化實現

一般會在Component中定義對父組件的引用,組合對象和葉子對象均可以繼承這個引用。那麼何時來維護這個引用呢?

較爲容易的辦法就是:在組合對象添加子組件對象的時候,爲子組件對象設置父組件的引用;在組合對象刪除一個子組件對象的時候,再從新設置相關子組件的父組件引用。把這些實現到Composite中,這樣全部的子類均可以繼承到這些方法,從而更容易的維護子組件到父組件的引用。

仍是看示例會比較清楚。在前面實現的商品類別的示例基礎上,來示例對父組件的引用,並實現刪除某個商品類別,而後把被刪除的商品類別對象的父商品類別,設置成爲被刪除的商品類別的子類別的父商品類別。也就是把被刪除的商品類別對象的子商品類別都上移一層。

  1. 先看看Component組件的定義,大體有以下變化:

添加一個屬性來記錄組件對象的父組件對象,同時提供相應的getter/setter方法來訪問父組件對象;

添加一個能獲取一個組件所包含的子組件對象的方法,提供給實現當某個組件被刪除時,把它的子組件對象上移一層的功能時使用;

public abstract class Component {
    /**
     * 記錄父組件對象
     */
    private Component parent = null;
    /**
     * 獲取一個組件的父組件對象
     * @return 一個組件的父組件對象
     */
    public Component getParent() {
       return parent;
    }
    /**
     * 設置一個組件的父組件對象
     * @param parent 一個組件的父組件對象
     */
    public void setParent(Component parent) {
       this.parent = parent;
    }
    /**
     * 返回某個組件的子組件對象
     * @return 某個組件的子組件對象
     */
    public List<Component> getChildren() {
       throw new UnsupportedOperationException("對象不支持這個功能");
    }
    /*-------------------如下是原有的定義----------------------*/
    public abstract void printStruct(String preStr);
    public void addChild(Component child) {
       throw new UnsupportedOperationException("對象不支持這個功能");
    }
    public void removeChild(Component child) {
       throw new UnsupportedOperationException("對象不支持這個功能");
    }
    public Component getChildren(int index) {
       throw new UnsupportedOperationException("對象不支持這個功能");
    }
}
  1. 接下來看看Composite的實現,大體有以下變化:

在添加子組件的方法實現裏面,加入對父組件的引用實現;

在刪除子組件的方法實現裏面,加入把被刪除的商品類別對象的父商品類別,設置成爲被刪除的商品類別的子類別的父商品類別的功能;

實現新的返回組件的子組件對象的功能;

/**
 * 組合對象,能夠包含其它組合對象或者葉子對象
 */
public class Composite extends Component{
    public void addChild(Component child) {
       //延遲初始化
       if (childComponents == null) {
           childComponents = new ArrayList<Component>();
       }
       childComponents.add(child);

       //添加對父組件的引用
       child.setParent(this);
    }

    public void removeChild(Component child) {
       if (childComponents != null) {
           //查找到要刪除的組件在集合中的索引位置
           int idx = childComponents.indexOf(child);
           if (idx != -1) {
              //先把被刪除的商品類別對象的父商品類別,
              //設置成爲被刪除的商品類別的子類別的父商品類別
              for(Component c : child.getChildren()){
                  //刪除的組件對象是本實例的一個子組件對象
                  c.setParent(this);
                  //把被刪除的商品類別對象的子組件對象添加到當前實例中
                  childComponents.add(c);
              }
            
              //真的刪除
              childComponents.remove(idx);
           }
       }     
    }
    public List<Component> getChildren() {
       return childComponents;
    }
    /*------------如下是原有的實現,沒有變化----------------*/
    private List<Component> childComponents = null;
    private String name = "";
    public Composite(String name){
       this.name = name;
    }
    public void printStruct(String preStr){
       System.out.println(preStr+"+"+this.name);
       if(this.childComponents!=null){
           preStr+=" ";     
           for(Component c : childComponents){
              c.printStruct(preStr);
           }
       }
    }
}
  1. 葉子對象沒有任何的改變,這裏就不去贅述了

  2. 能夠來寫個客戶端測試一下了,在原來的測試後面,刪除一個節點,而後再次輸出整棵樹的結構,看看效果。示例代碼以下:

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("");
       System.out.println("---------------------------->");
       //而後刪除一個節點
       root.removeChild(c1);
       //從新輸出整棵樹
       root.printStruct("");
    }
}

運行結果以下:

+服裝
    +男裝
        -襯衣
        -夾克
    +女裝
        -裙子
        -套裝
---------------------------->
+服裝
    +女裝
        -裙子
        -套裝
    -襯衣
    -夾克

仔細觀察上面的結果,當男裝的節點被刪除後,會把原來男裝節點下的子節點,添加到原來男裝的父節點,也就是服裝的下面了。輸出是按照添加的前後順序來的,因此先輸出了女裝的,而後纔是襯衣和夾克節點。

##3.4 環狀引用## 所謂環狀引用指的是:在對象結構中,某個對象包含的子對象,或是子對象的子對象,或是子對象的子對象的子對象……,如此通過N層後,出現所包含的子對象中有這個對象自己,從而構成了環狀引用。好比:A包含B,B包含C,而C又包含了A,轉了一圈,轉回來了,就構成了一個環狀引用。

這個在使用組合模式構建樹狀結構的時候,是須要考慮的一種狀況。一般狀況下,組合模式構建的樹狀結構,是不該該出現環狀引用的,若是出現了,多半是有錯誤發生了。所以在應用組合模式實現功能的時候,就應該考慮要檢測並避免出現環狀引用,不然很容易引發死循環的操做,或是同一個功能被操做屢次。

可是要說明的是:組合模式的實現裏面也是能夠有環狀引用的,固然須要特殊構建環狀引用,並提供相應的檢測和處理,這裏不去討論這種狀況。

那麼該如何檢測是否有環狀引用的狀況發生呢?

一個很簡單的思路就是記錄下每一個組件從根節點開始的路徑,由於要出現環狀引用,在一條路徑上,某個對象就必然會出現兩次。所以只要每一個對象在整個路徑上只是出現了一次,那麼就不會出現環狀引用。

這個判斷的功能能夠添加到Composite對象的添加子組件的方法中,若是是環狀引用的話,就拋出例外,並不會把它加入到子組件中去。

仍是經過示例來講明吧。在前面實現的商品類別的示例基礎上,來加入對環狀引用的檢測和處理。約定用組件的名稱來表明組件,也就是說,組件的名稱是惟一的,不會重複的,只要檢測在一條路徑上,組件名稱不會重複,那麼組件就不會重複。

  1. 先看看Component的定義,大體有以下的變化:

添加一個記錄每一個組件的路徑的屬性,並提供相應的getter/setter方法;

爲了拼接組件的路徑,新添加一個方法來獲取組件的名稱;

public abstract class Component {
    /**
     * 記錄每一個組件的路徑
     */
    private String componentPath = "";
    /**
     * 獲取組件的路徑
     * @return 組件的路徑
     */
    public String getComponentPath() {
       return componentPath;
    }
    /**
     * 設置組件的路徑
     * @param componentPath 組件的路徑
     */
    public void setComponentPath(String componentPath) {
       this.componentPath = componentPath;
    }
    /**
     * 獲取組件的名稱
     * @return 組件的名稱
     */
    public abstract String getName();
    /*-------------------如下是原有的定義----------------------*/    
    public abstract void printStruct(String preStr);
    public void addChild(Component child) {
       throw new UnsupportedOperationException("對象不支持這個功能");
    }
    public void removeChild(Component child) {
       throw new UnsupportedOperationException("對象不支持這個功能");
    }
    public Component getChildren(int index) {
       throw new UnsupportedOperationException("對象不支持這個功能");
    }
}
  1. 再看看Composite的實現,大體有以下的變化:

提供獲取組件名稱的實現;

在添加子組件的實現方法裏面,進行是否環狀引用的判斷,並計算組件對象的路徑,而後設置回組件對象去;

public class Composite extends Component{
    public String getName(){
       return this.name;
    }
    public void addChild(Component child) {
       //延遲初始化
       if (childComponents == null) {
           childComponents = new ArrayList<Component>();
       }
       childComponents.add(child);    
     
       //先判斷組件路徑是否爲空,若是爲空,說明本組件是根組件
       if(this.getComponentPath()==null || this.getComponentPath().trim().length()==0){
           //把本組件的name設置到組件路徑中
           this.setComponentPath(this.name);
       }
       //判斷要加入的組件在路徑上是否出現過
       //先判斷是不是根組件
       if(this.getComponentPath().startsWith(child.getName()+".")){
           //說明是根組件,重複添加了
           throw new java.lang.IllegalArgumentException("在本通路上,組件 '"+child.getName()+"' 已被添加過了");
       } else {
           if(this.getComponentPath().indexOf("."+child.getName()) < 0){
              //表示沒有出現過,那麼能夠加入
              //計算組件的路徑
              String componentPath = this.getComponentPath()+"."+child.getName();
              //設置子組件的路徑
              child.setComponentPath(componentPath);
           }else{
              throw new java.lang.IllegalArgumentException("在本通路上,組件 '"+child.getName()+"' 已被添加過了");
           }     
       }
    }
    /*---------------如下是原有的實現,沒有變化------------------*/
    private List<Component> childComponents = null;
    private String name = "";
    public Composite(String name){
       this.name = name;
    }
    public void printStruct(String preStr){
       System.out.println(preStr+"+"+this.name);
       if(this.childComponents!=null){
           preStr+=" ";     
           for(Component c : childComponents){
              c.printStruct(preStr);
           }
       }
    }
}
  1. 葉子對象的實現,只是多了一個實現獲取組件名稱的方法,也就是直接返回葉子對象的Name,跟Composite中的實現是相似的,就不去代碼示例了

  2. 客戶端的代碼能夠不作修改,能夠正常執行,輸出商品類別樹來。固然,若是想要看到環狀引用檢測的效果,你能夠作一個環狀引用測試看看,好比:

public class Client {
    public static void main(String[] args) {
       //定義全部的組合對象
       Component root = new Composite("服裝");
       Component c1 = new Composite("男裝");
       Component c2= new Composite("襯衣");
       Component c3= new Composite("男裝");
       //設置一個環狀引用
       root.addChild(c1);
       c1.addChild(c2);
       c2.addChild(c3);
     
       //調用根對象的輸出功能來輸出整棵樹
       root.printStruct("");
    }
}

運行結果以下:

Exception in thread "main" java.lang.IllegalArgumentException: 在本通路上,組件 '男裝' 已被添加過了;
後面的堆棧信息就省略了
  1. 說明

上面進行環路檢測的實現是很是簡單的,可是還有一些問題沒有考慮,好比:要是刪除了路徑上的某個組件對象,那麼全部該組件對象的子組件對象所記錄的路徑,都須要修改,要把這個組件從全部相關路徑上都去除掉。就是在被刪除的組件對象的全部子組件對象的路徑上,查找到被刪除組件的名稱,而後經過字符串截取的方式把它刪除掉

只是這樣的實現方式有些不太好,要實現這樣的功能,能夠考慮使用動態計算路徑的方式,每次添加一個組件的時候,動態的遞歸尋找父組件,而後父組件再找父組件,一直到根組件,這樣就能避免某個組件被刪除後,路徑發生了變化,須要修改全部相關路徑記錄的狀況。

##3.5 組合模式的優缺點##

  1. 定義了包含基本對象和組合對象的類層次結構

在組合模式中,基本對象能夠被組合成更復雜的組合對象,而組合對象又能夠組合成更復雜的組合對象,能夠不斷地遞歸組合下去,從而構成一個統一的組合對象的類層次結構。

  1. 統一了組合對象和葉子對象

在組合模式中,能夠把葉子對象看成特殊的組合對象看待,爲它們定義統一的父類,從而把組合對象和葉子對象的行爲統一塊兒來

  1. 簡化了客戶端調用

組合模式經過統一組合對象和葉子對象,使得客戶端在使用它們的時候,就不須要再去區分它們,客戶不關心使用的究竟是什麼類型的對象,這就大大簡化了客戶端的使用。

  1. 更容易擴展

因爲客戶端是統一的面對Component來操做,所以,新定義的Composite或Leaf子類可以很容易的與已有的結構一塊兒工做,而客戶端不須要爲增添了新的組件類而改變。

  1. 很難限制組合中的組件類型

容易增長新的組件也會帶來一些問題,好比很難限制組合中的組件類型。這在須要檢測組件類型的時候,使得咱們不能依靠編譯期的類型約束來完成,必須在運行期間動態檢測。

##3.6 思考組合模式##

  1. 組合模式的本質

組合模式的本質:統一葉子對象和組合對象。

組合模式經過把葉子對象當成特殊的組合對象看待,從而對葉子對象和組合對象一視同仁,通通當成了Component對象,有機的統一了葉子對象和組合對象。

正是由於統一了葉子對象和組合對象,在將對象構建成樹形結構的時候,纔不須要作區分,反正是組件對象裏面包含其它的組件對象,如此遞歸下去;也才使得對於樹形結構的操做變得簡單,無論對象類型,統一操做。

  1. 什麼時候選用組合模式

建議在以下狀況中,選用組合模式:

若是你想表示對象的部分-總體層次結構,能夠選用組合模式,把總體和部分的操做統一塊兒來,使得層次結構實現更簡單,從外部來使用這個層次結構也簡單;

若是你但願統一的使用組合結構中的全部對象,能夠選用組合模式,這正是組合模式提供的主要功能;

##3.7 相關模式##

  1. 組合模式和裝飾模式

這兩個模式能夠組合使用。

裝飾模式在組裝多個裝飾器對象的時候,是一個裝飾器找下一個裝飾器,下一個再找下一個,如此遞歸下去。那麼這種結構也可使用組合模式來幫助構建,這樣一來,裝飾器對象就至關於組合模式的Composite對象了。

要讓兩個模式能很好的組合使用,一般會讓它們有一個公共的父類,所以裝飾器必須支持組合模式須要的一些功能,好比:增長、刪除子組件等等。

  1. 組合模式和享元模式

這兩個模式能夠組合使用。

若是組合模式中出現大量類似的組件對象的話,能夠考慮使用享元模式來幫助緩存組件對象,這能夠減小對內存的須要。

使用享元模式也是有條件的,若是組件對象的可變化部分的狀態可以從組件對象裏面分離出去,並且組件對象自己不須要向父組件發送請求的話,就能夠採用享元模式。

  1. 組合模式和迭代器模式

這兩個模式能夠組合使用。

在組合模式中,一般可使用迭代器模式來遍歷組合對象的子對象集合,而無需關心具體存放子對象的聚合結構

  1. 組合模式和訪問者模式

這兩個模式能夠組合使用。

訪問者模式可以在不修改原有對象結構的狀況下,給對象結構中的對象增添新的功能。將訪問者模式和組合模式合用,能夠把本來分散在Composite和Leaf類中的操做和行爲都局部化。

若是在使用組合模式的時候,預計到從此可能會有增添其它功能的可能,那麼能夠採用訪問者模式,來預留好添加新功能的方式和通道,這樣之後在添加新功能的時候,就不須要再修改已有的對象結構和已經實現的功能了。

  1. 組合模式和職責鏈模式

這兩個模式能夠組合使用。

職責鏈模式要解決的問題是:實現請求的發送者和接收者之間解耦。職責鏈模式的實現方式是把多個接收者組合起來,構成職責鏈,而後讓請求在這條鏈上傳遞,直到有接收者處理這個請求爲止。

能夠應用組合模式來構建這條鏈,至關因而子組件找父組件,父組件又找父組件,如此遞歸下去,構成一條處理請求的組件對象鏈。

  1. 組合模式和命令模式

這兩個模式能夠組合使用。

命令模式中有一個宏命令的功能,一般這個宏命令就是使用組合模式來組裝出來的。

相關文章
相關標籤/搜索