【結構型模式十】組合模式(Composite)

1 場景問題#

1.1 商品類別樹##

考慮這樣一個實際的應用:管理商品類別樹。

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

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

仔細觀察上面的商品類別樹,有以下幾個明顯的特點:

有一個根節點,比如服裝,它沒有父節點,它可以包含其它的節點;

樹枝節點,有一類節點可以包含其它的節點,稱之為樹枝節點,比如男裝、女裝;

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

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

1.2 不用模式的解決方案##

要管理商品類別樹,就是要管理樹的各個節點,現在樹上的節點有三類,根節點、樹枝節點和葉子節點,再進一步分析發現,根節點和樹枝節點是類似的,都是可以包含其它節點的節點,把它們稱為容器節點。

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

然后在組合對象和葉子對象里面去實現要求的功能就可以了,看看代碼實現。

  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 模式結構和說明##

組合模式的結構如圖所示:

組合模式的結構

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

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

Composite:組合對象,通常會存儲子組件,定義包含子組件的那些組件的行為,并實現在組件接口中定義的與子組件有關的操作。

Client:客戶端,通過組件接口來操作組合結構里面的組件對象。

一種典型的Composite對象結構通常是如圖15.2所示的樹形結構,一個Composite對象可以包含多個葉子多象和其它的Composite對象,雖然15.2的圖看起來好像有些對稱,但是那只是為了讓圖看起來美觀一點,并不是說Composite組合的對象結構就是這樣對稱的,這點要提前說明一下。

典型的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. 接下來看看組合對象的實現,這個對象變化就比較多,大致有如下的改變:
  1. 新的Composite對象需要繼承組件對象;

  2. 原來用來記錄包含的其它組合對象的集合,和包含的其它葉子對象的集合,這兩個集合被合并成為一個,就是統一的包含其它子組件對象的集合。使用組合模式來實現,不再需要區分到底是組合對象還是葉子對象了;

  3. 原來的addComposite和addLeaf的方法,可以不需要了,合并實現成組件對象中定義的方法addChild,當然需要現在的Composite來實現這個方法。使用組合模式來實現,不再需要區分到底是組合對象還是葉子對象了;

  4. 原來的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. 組合模式和迭代器模式

這兩個模式可以組合使用。

在組合模式中,通??梢允褂玫髂J絹肀闅v組合對象的子對象集合,而無需關心具體存放子對象的聚合結構。

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

這兩個模式可以組合使用。

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

如果在使用組合模式的時候,預計到今后可能會有增添其它功能的可能,那么可以采用訪問者模式,來預留好添加新功能的方式和通道,這樣以后在添加新功能的時候,就不需要再修改已有的對象結構和已經實現的功能了。

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

這兩個模式可以組合使用。

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

可以應用組合模式來構建這條鏈,相當于是子組件找父組件,父組件又找父組件,如此遞歸下去,構成一條處理請求的組件對象鏈。

  1. 組合模式和命令模式

這兩個模式可以組合使用。

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容