「補課」進行時:設計模式(14)——組合模式

1. 前文匯總

「補課」進行時:設計模式系列

2. 某東的菜單

前段時間雙十一,不知道各位的戰果如何,反正我是屯了兩盒口罩湊個數。

電商平台為我們提供的方便快捷的搜索框入口,我想大多數人在使用的時候應該都會使用這個入口,但其實電商平台還為我們提供了另一個入口,就是它的分類體系,如下:

我簡單抽象一下:

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

可以看到,這是一個樹結構,在前端實現一個這種菜單樹可以選用 ZTree 插件,做過前端的都知道。

下面,用 Java 程式碼實現一下,輸出一下上面的這個樹狀結構:

觀察這個樹狀結構,可以看到節點分為三種類型:

  • 根節點
  • 樹枝節點
  • 葉子節點(沒有子節點)

根節點和樹枝節點的構造是比較類似的,都是可以有子節點,這兩個節點可以抽象成一個對象。

首先定義一個葉子節點:

public class Leaf {
    // 葉子對象的名字
    private String name;
    // 構造方法
    public Leaf(String name) {
        this.name = name;
    }
    // 輸出葉子對象的結構,葉子對象沒有子對象,也就是輸出葉子對象的名字
    public void printStruct(String preStr) {
        System.out.println(preStr + " - " + name);
    }
}

接著定義一個組合對象:

public class Composite {
    // 組合對象集合
    private Collection<Composite> childComposite = new ArrayList<>();
    // 葉子對象集合
    private Collection<Leaf> childLeaf = new ArrayList<>();
    // 組合對象名稱
    private String name;
    // 構造函數
    public Composite(String name) {
        this.name = name;
    }
    // 向組合對象加入被它包含的其它組合對象
    public void addComposite(Composite c){
        this.childComposite.add(c);
    }
    // 向組合對象加入被它包含的葉子對象
    public void addLeaf(Leaf leaf){
        this.childLeaf.add(leaf);
    }
    // 輸出自身結構
    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);
        }
    }
}

來一個測試類:

public class Test {
    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("");
    }
}

上面的這種實現方案,雖然能實現了我們希望看到的功能,但是有一個很明顯的問題:那就是必須區分組合對象和葉子對象,並進行有區別的對待。

3. 組合模式

3.1 定義

組合模式(Composite Pattern)也叫合成模式,有時又叫做部分-整體模式(Part-Whole),主要是用來描述部分與整體的關係,其定義如下:

Compose objects into tree structures to represent part-wholehierarchies.Composite lets clients treat individual objects and compositionsof objects uniformly.(將對象組合成樹形結構以表示「部分-整體」的層次結構,使得用戶對單個對象和組合對象的使用具有一致性。)

3.2 通用類圖

  • Component 抽象構件角色: 定義參加組合對象的共有方法和屬性,可以定義一些默認的行為或屬性。
  • Leaf 葉子構件: 葉子對象,其下再也沒有其他的分支,也就是遍歷的最小單位。
  • Composite 樹枝構件: 樹枝對象,它的作用是組合樹枝節點和葉子節點形成一個樹形結構。

3.3 通用程式碼

public abstract class Component {
    // 整體和個體都共享的邏輯
    void doSomething() {
        // 具體的業務邏輯
    }
}

public class Leaf extends Component {
    // 可以複寫父類的方法
    @Override
    void doSomething() {
        super.doSomething();
    }
}

public class Composite extends Component {
    // 構建容器
    private ArrayList<Component> componentArrayList = new ArrayList<>();
    // 增加一個葉子構件或者樹枝構件
    public void add(Component component) {
        this.componentArrayList.add(component);
    }
    // 刪除一個葉子構件或者樹枝構件
    public void remove(Component component) {
        this.componentArrayList.remove(component);
    }
    // 獲得分支下所有葉子構件或者樹枝構件

    public ArrayList<Component> getChildren() {
        return this.componentArrayList;
    }
}

3.4 優點

  1. 高層模組調用簡單:

一棵樹形機構中的所有節點都是 Component ,局部和整體對調用者來說沒有任何區別,也就是說,高層模組不必關心自己處理的是單個對象還是整個組合結構,簡化了高層模組的程式碼。

  1. 節點自由增加:

使用了組合模式後,我們可以看看,如果想增加一個樹枝節點、樹葉節點是不是都很容易,只要找到它的父節點就成,非常容易擴展,符合開閉原則,對以後的維護非常有利。

4. 示例改進(安全模式)

把最上面的示例修改成組合模式,首先需要定義一個抽象組件:

public abstract class Component {
    // 輸出組件名稱
    abstract void printStruct(String preStr);
}

葉子節點:

public class Leaf extends Component {

    private String name;

    public Leaf(String name) {
        this.name = name;
    }

    @Override
    void printStruct(String preStr) {
        System.out.println(preStr + " - " + name);
    }
}

樹枝節點:

public class Composite extends Component{

    // 組合對象集合
    private Collection<Component> childComponents;

    // 組合對象的名字
    private String name;

    public Composite(String name) {
        this.name = name;
    }

    public void addChild(Component child) {
        if (this.childComponents == null) {
            this.childComponents = new ArrayList<>();
        }
        this.childComponents.add(child);
    }

    void removeChild(Component child) {
        this.childComponents.remove(child);
    }

    Collection getChildren() {
        return this.childComponents;
    }

    @Override
    void printStruct(String preStr) {
        System.out.println(preStr + " + " + this.name);
        if (this.childComponents != null) {
            preStr+=" ";
            for(Component c : this.childComponents){
                //遞歸輸出每個子對象
                c.printStruct(preStr);
            }
        }
    }
}

測試類:

public class Test {
    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("");
    }
}

5. 透明模式

組合模式有兩種不同的實現,安全模式和透明模式,上面的示例是安全模式,下面是透明模式的通用類圖:

和上面的那個安全模式的通用類圖對比一下區別,就非常明顯了,只是單純的把幾個方法 addChild()removeChild()getChildren() 幾個方法放到了抽象類中。

在透明模式中不管葉子對象還是樹枝對象都有相同的結構,通過判斷是否存在子節點來判斷葉子節點還是樹枝節點,如果處理不當,這個會在運行期出現問題。

而在安全模式中,它是把樹枝節點和樹葉節點徹底分開,樹枝節點單獨擁有用來組合的方法,這種方案比較安全。

抽象角色:

public abstract class Component {
    // 輸出組件名稱
    abstract void printStruct(String preStr);

    // 向組合對象中加入組件對象
    abstract void addChild(Component child);

    // 從組合對象中移出某個組件對象
    abstract void removeChild(Component child);

    // 返回組件對象
    abstract Collection getChildren();
}

葉子節點:

public class Leaf extends Component {

    private String name;

    public Leaf(String name) {
        this.name = name;
    }

    // 向組合對象中加入組件對象
    @Deprecated
    public void addChild(Component child) {
        // 預設實現,如果子類未實現此功能,由父類拋出異常
        throw new UnsupportedOperationException("對象不支援這個功能");
    }

    // 從組合對象中移出某個組件對象
    @Deprecated
    public void removeChild(Component child){
        // 預設實現,如果子類未實現此功能,由父類拋出異常
        throw new UnsupportedOperationException("對象不支援這個功能");
    }

    @Deprecated
    Collection getChildren() {
        throw new UnsupportedOperationException("對象不支援這個功能");
    }

    @Override
    void printStruct(String preStr) {
        System.out.println(preStr + " - " + name);
    }
}

這裡使用 Deprecated 註解是為了在編譯期告訴調用者,這方法我有,可以調用,但是已經失效了,如果一定要調用,那麼在運行期會拋出 UnsupportedOperationException 的錯誤。

樹枝節點:

public class Composite extends Component {

    // 組合對象集合
    private Collection<Component> childComponents;

    // 組合對象的名字
    private String name;

    public Composite(String name) {
        this.name = name;
    }

    public void addChild(Component child) {
        if (this.childComponents == null) {
            this.childComponents = new ArrayList<>();
        }
        this.childComponents.add(child);
    }

    @Override
    void removeChild(Component child) {
        this.childComponents.remove(child);
    }

    @Override
    Collection getChildren() {
        return this.childComponents;
    }

    @Override
    void printStruct(String preStr) {
        System.out.println(preStr + " + " + this.name);
        if (this.childComponents != null) {
            preStr+=" ";
            for(Component c : this.childComponents){
                //遞歸輸出每個子對象
                c.printStruct(preStr);
            }
        }
    }
}

測試類:

public class Test {
    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("");
    }
}

6. 參考

//www.jianshu.com/p/dead42334033