組合模式

1  場景問題

15.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);
    }
}

(2)再來看看組合對象的代碼實現,組合對象裏面可以包含其它的組合對象或者是葉子對象,由於類型不一樣,需要分開記錄。示例代碼如下:

/**
 * 組合對象,可以包含其它組合對象或者葉子對象
 */
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);
       }
    }
}

(3)寫個客戶端來測試一下,看看是否能實現要求的功能,示例代碼如下:

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)組合模式定義

wKiom1lnOougE6vMAABujcNsYZ0523.png

(2)應用組合模式來解決的思路

       仔細分析上面不用模式的例子中,要區分組合對象和葉子對象的根本原因,就在於沒有把組合對象和葉子對象統一起來,也就是說,組合對象類型和葉子對象類型是完全不同的類型,這導致了操作的時候必須區分它們。

       組合模式通過引入一個抽象的組件對象,作爲組合對象和葉子對象的父對象,這樣就把組合對象和葉子對象統一起來了,用戶使用的時候,始終是在操作組件對象,而不再去區分是在操作組合對象還是在操作葉子對象。

組合模式的關鍵就在於這個抽象類,這個抽象類既可以代表葉子對象,也可以代表組合對象,這樣用戶在操作的時候,對單個對象和組合對象的使用就具有了一致性。

2.2  模式結構和說明

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

wKiom1lnOsehhxxLAAEQqxZaigw132.png

Component

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

Leaf

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

Composite

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

Client

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

 

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

wKioL1lnOv3wstFeAADX7P40A1k341.png

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);
    }
}

2.4  使用組合模式重寫示例

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

爲了整體理解和把握整個示例,先來看看示例的整體結構,如圖所示:

wKiom1lnO-rwML0aAAEsCDjH9To656.png

(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("");
    }
}

通過上面的示例,大家可以看出,通過使用組合模式,把一個“部分-整體”的層次結構表示成了對象樹的結構,這樣一來,客戶端就無需再區分操作的是組合對象還是葉子對象了,對於客戶端而言,操作的都是組件對象。

3  模式講解

3.1  認識組合模式

(1)組合模式的目的

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

實現這個目標的關鍵之處,是設計一個抽象的組件類,讓它可以代表組合對象和葉子對象。這樣一來,客戶端就不用區分到底是組合對象還是葉子對象了,只需要全部當成組件對象進行統一的操作就可以了。

(2)對象樹

通常,組合模式會組合出樹形結構來,組成這個樹形結構所使用的多個組件對象,就自然的形成了對象樹。

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

(3)組合模式中的遞歸

組合模式中的遞歸,指的是對象遞歸組合,不是常說的遞歸算法。通常我們談的遞歸算法,是指“一個方法會調用方法自己”這樣的算法,是從功能上來講的,比如那個經典的求階乘的例子,示例如下:

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);
    }
}

而這裏的組合模式中的遞歸,是對象本身的遞歸,是對象的組合方式,是從設計上來講的,在設計上稱作遞歸關聯,是對象關聯關係的一種。

3.2 父組件引用

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

  • 現在要刪除某個商品類別。如果這個類別沒有子類別的話,直接刪除就好了,沒有太大的問題,但是如果它還有子類別,這就涉及到它的子類別如何處理了,一種情況是連帶全部刪除,一種是上移一層,把被刪除的商品類別對象的父商品類別,設置成爲被刪除的商品類別的子類別的父商品類別。

  • 現在要進行商品類別的細化和調整,把原本屬於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(
"對象不支持這個功能");
    }
}

(2)接下來看看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);
           }
       }
    }
}

(3)葉子對象沒有任何的改變,這裏就不去贅述了

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

運行結果如下:

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

3.3  環狀引用

   所謂環狀引用指的是:在對象結構中,某個對象包含的子對象,或是子對象的子對象,或是子對象的子對象的子對象……,如此經過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(
"對象不支持這個功能");
    }
}

(2)再看看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);
           }
       }
    }
}

(3)葉子對象的實現,只是多了一個實現獲取組件名稱的方法,也就是直接返回葉子對象的Name,跟Composite中的實現是類似的,就不去代碼示例了

(4)客戶端的代碼可以不做修改,可以正常執行,輸出商品類別樹來。當然,如果想要看到環狀引用檢測的效果,你可以做一個環狀引用測試看看,比如:

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: 在本通路上,組件 '男裝' 已被添加過了
    後面的堆棧信息就省略了

(5)說明

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

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

3.4  組合模式的優缺點

l          定義了包含基本對象和組合對象的類層次結構
    在組合模式中,基本對象可以被組合成更復雜的組合對象,而組合對象又可以組合成更復雜的組合對象,可以不斷地遞歸組合下去,從而構成一個統一的組合對象的類層次結構

l          統一了組合對象和葉子對象
    在組合模式中,可以把葉子對象當作特殊的組合對象看待,爲它們定義統一的父類,從而把組合對象和葉子對象的行爲統一起來

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

l          更容易擴展
    由於客戶端是統一的面對Component來操作,因此,新定義的Composite或Leaf子類能夠很容易的與已有的結構一起工作,而客戶端不需要爲增添了新的組件類而改變

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

3.5  思考組合模式

1:組合模式的本質

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

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

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

2:何時選用組合模式

       建議在如下情況中,選用組合模式:

  • 如果你想表示對象的部分-整體層次結構,可以選用組合模式,把整體和部分的操作統一起來,使得層次結構實現更簡單,從外部來使用這個層次結構也簡單

  • 如果你希望統一的使用組合結構中的所有對象,可以選用組合模式,這正是組合模式提供的主要功能

3.6  相關模式

l          組合模式和裝飾模式
    這兩個模式可以組合使用。
    裝飾模式在組裝多個裝飾器對象的時候,是一個裝飾器找下一個裝飾器,下一個再找下一個,如此遞歸下去。那麼這種結構也可以使用組合模式來幫助構建,這樣一來,裝飾器對象就相當於組合模式的Composite對象了。
    要讓兩個模式能很好的組合使用,通常會讓它們有一個公共的父類,因此裝飾器必須支持組合模式需要的一些功能,比如:增加、刪除子組件等等。

l          組合模式和享元模式
    這兩個模式可以組合使用。
    如果組合模式中出現大量相似的組件對象的話,可以考慮使用享元模式來幫助緩存組件對象,這可以減少對內存的需要。
    使用享元模式也是有條件的,如果組件對象的可變化部分的狀態能夠從組件對象裏面分離出去,而且組件對象本身不需要向父組件發送請求的話,就可以採用享元模式。

l          組合模式和迭代器模式
    這兩個模式可以組合使用。
    在組合模式中,通常可以使用迭代器模式來遍歷組合對象的子對象集合,而無需關心具體存放子對象的聚合結構。

l          組合模式和訪問者模式
    這兩個模式可以組合使用。
    訪問者模式能夠在不修改原有對象結構的情況下,給對象結構中的對象增添新的功能。將訪問者模式和組合模式合用,可以把原本分散在Composite和Leaf類中的操作和行爲都局部化。
    如果在使用組合模式的時候,預計到今後可能會有增添其它功能的可能,那麼可以採用訪問者模式,來預留好添加新功能的方式和通道,這樣以後在添加新功能的時候,就不需要再修改已有的對象結構和已經實現的功能了。

l          組合模式和職責鏈模式
    這兩個模式可以組合使用。
    職責鏈模式要解決的問題是:實現請求的發送者和接收者之間解耦。職責鏈模式的實現方式是把多個接收者組合起來,構成職責鏈,然後讓請求在這條鏈上傳遞,直到有接收者處理這個請求爲止。
    可以應用組合模式來構建這條鏈,相當於是子組件找父組件,父組件又找父組件,如此遞歸下去,構成一條處理請求的組件對象鏈。

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


轉載至:http://sishuok.com/forum/blogPost/list/5513.html

   cc老師的設計模式是我目前看過最詳細最有實踐的教程。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章