設計模式(十四)----結構型模式之組合模式

2023-03-02 21:00:36

1 概述

對於這個圖片肯定會非常熟悉,上圖我們可以看做是一個檔案系統,對於這樣的結構我們稱之為樹形結構。在樹形結構中可以通過呼叫某個方法來遍歷整個樹,當我們找到某個葉子節點後,就可以對葉子節點進行相關的操作。可以將這顆樹理解成一個大的容器,容器裡面包含很多的成員物件,這些成員物件即可是容器物件也可以是葉子物件。但是由於容器物件和葉子物件在功能上面的區別,使得我們在使用的過程中必須要區分容器物件和葉子物件,但是這樣就會給客戶帶來不必要的麻煩,作為客戶而已,它始終希望能夠一致的對待容器物件和葉子物件。

定義:

又名部分整體模式,是用於把一組相似的物件當作一個單一的物件。組合模式依據樹形結構來組合物件,用來表示部分以及整體層次。這種型別的設計模式屬於結構型模式,它建立了物件組的樹形結構。

2 結構

組合模式主要包含三種角色:

  • 抽象根節點(Component):定義系統各層次物件的共有方法和屬性,可以預先定義一些預設行為和屬性。

  • 樹枝節點(Composite):定義樹枝節點的行為,儲存子節點,組合樹枝節點和葉子節點形成一個樹形結構。

  • 葉子節點(Leaf):葉子節點物件,其下再無分支,是系統層次遍歷的最小單位。

3 案例實現

【例】軟體選單

如下圖,我們在存取別的一些管理系統時,經常可以看到類似的選單。一個選單可以包含選單項(選單項是指不再包含其他內容的選單條目),也可以包含帶有其他選單項的選單,因此使用組合模式描述選單就很恰當,我們的需求是針對一個選單,列印出其包含的所有選單以及選單項的名稱。

要實現該案例,我們先畫出類圖:

程式碼實現:

不管是選單還是選單項,都應該繼承自統一的介面,這裡姑且將這個統一的介面稱為選單元件。

//選單元件  不管是選單還是選單項,都應該繼承該類 抽象根節點
public abstract class MenuComponent {
​
    protected String name;
    protected int level;
​
    //新增選單
    public void add(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }
​
    //移除選單
    public void remove(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }
​
    //獲取指定的子選單
    public MenuComponent getChild(int i){
        throw new UnsupportedOperationException();
    }
​
    //獲取選單名稱
    public String getName(){
        return name;
    }
​
    //列印選單名稱的方法(包含子選單和字選單項)
    public void print(){
        throw new UnsupportedOperationException();
    }
}

這裡的MenuComponent定義為抽象類,因為有一些共有的屬性和行為要在該類中實現,Menu和MenuItem類就可以只覆蓋自己感興趣的方法,而不用搭理不需要或者不感興趣的方法,舉例來說,Menu類可以包含子選單,因此需要覆蓋add()、remove()、getChild()方法,但是MenuItem就不應該有這些方法。這裡給出的預設實現是丟擲異常,你也可以根據自己的需要改寫預設實現。

public class Menu extends MenuComponent {
​
    private List<MenuComponent> menuComponentList;
​
    public Menu(String name,int level){
        this.level = level;
        this.name = name;
        menuComponentList = new ArrayList<MenuComponent>();
    }
​
    @Override
    public void add(MenuComponent menuComponent) {
        menuComponentList.add(menuComponent);
    }
​
    @Override
    public void remove(MenuComponent menuComponent) {
        menuComponentList.remove(menuComponent);
    }
​
    @Override
    public MenuComponent getChild(int i) {
        return menuComponentList.get(i);
    }
​
    @Override
    public void print() {
​
        //列印選單名稱
        for (int i = 1; i < level; i++) {
            System.out.print("--");
        }
        System.out.println(name);
        //列印子選單或者子選單項名稱
        for (MenuComponent menuComponent : menuComponentList) {
            menuComponent.print();
        }
    }
}

Menu類已經實現了除了getName方法的其他所有方法,因為Menu類具有新增選單,移除選單和獲取子選單的功能。

public class MenuItem extends MenuComponent {
​
    public MenuItem(String name,int level) {
        this.name = name;
        this.level = level;
    }
​
    @Override
    public void print() {
        for (int i = 1; i < level; i++) {
            System.out.print("--");
        }
        System.out.println(name);
    }
}

MenuItem是選單項,不能再有子選單,所以新增選單,移除選單和獲取子選單的功能並不能實現。

測試類

public class Client {
    public static void main(String[] args) {
        //建立選單樹
        MenuComponent menu1 = new Menu("選單管理",2);
        menu1.add(new MenuItem("頁面存取",3));
        menu1.add(new MenuItem("展開選單",3));
        menu1.add(new MenuItem("編輯選單",3));
        menu1.add(new MenuItem("刪除選單",3));
        menu1.add(new MenuItem("新增選單",3));
​
        MenuComponent menu2 = new Menu("許可權管理",2);
        menu2.add(new MenuItem("頁面存取",3));
        menu2.add(new MenuItem("提交儲存",3));
​
        MenuComponent menu3 = new Menu("角色管理",2);
        menu3.add(new MenuItem("頁面存取",3));
        menu3.add(new MenuItem("新增角色",3));
        menu3.add(new MenuItem("修改角色",3));
​
        //建立一級選單
        MenuComponent component = new Menu("系統管理",1);
        //將二級選單新增到一級選單中
        component.add(menu1);
        component.add(menu2);
        component.add(menu3);
​
​
        //列印選單名稱(如果有子選單一塊列印)
        component.print();
    }
}
​

測試結果

4 組合模式的分類

在使用組合模式時,根據抽象構件類的定義形式,我們可將組合模式分為透明組合模式和安全組合模式兩種形式。

  • 透明組合模式

    透明組合模式中,抽象根節點角色中宣告了所有用於管理成員物件的方法,比如在範例中 MenuComponent 宣告了 addremovegetChild 方法,這樣做的好處是確保所有的構件類都有相同的介面。透明組合模式也是組合模式的標準形式。

    透明組合模式的缺點是不夠安全,因為葉子物件和容器物件在本質上是有區別的,葉子物件不可能有下一個層次的物件,即不可能包含成員物件,因此為其提供 add()、remove() 等方法是沒有意義的,這在編譯階段不會出錯,但在執行階段如果呼叫這些方法可能會出錯(如果沒有提供相應的錯誤處理程式碼)

  • 安全組合模式

    在安全組合模式中,在抽象構件角色中沒有宣告任何用於管理成員物件的方法,而是在樹枝節點 Menu 類中宣告並實現這些方法。安全組合模式的缺點是不夠透明,因為葉子構件和容器構件具有不同的方法,且容器構件中那些用於管理成員物件的方法沒有在抽象構件類中定義,因此使用者端不能完全針對抽象程式設計,必須有區別地對待葉子構件和容器構件。

 

5 優點

  • 組合模式可以清楚地定義分層次的複雜物件,表示物件的全部或部分層次,它讓使用者端忽略了層次的差異,方便對整個層次結構進行控制。

  • 使用者端可以一致地使用一個組合結構或其中單個物件,不必關心處理的是單個物件還是整個組合結構,簡化了使用者端程式碼。

  • 在組合模式中增加新的樹枝節點和葉子節點都很方便,無須對現有類庫進行任何修改,符合「開閉原則」。

  • 組合模式為樹形結構的物件導向實現提供了一種靈活的解決方案,通過葉子節點和樹枝節點的遞迴組合,可以形成複雜的樹形結構,但對樹形結構的控制卻非常簡單。

6 使用場景

組合模式正是應樹形結構而生,所以組合模式的使用場景就是出現樹形結構的地方。比如:檔案目錄顯示,多級目錄呈現等樹形結構資料的操作。