【設計模式】第九篇:組合模式解決層級關係結構問題

BWH_Steven發表於2020-11-30

說明:此文章兩個示例中,公司結構的示例思路來自於《大話設計模式》,內容以及程式碼經過了一定修改,尊重且維護作者版權所有,特此宣告。

一 引言

在生活中常常會見到一些具有層級關係的結構,例如學生時代的【大學-學院-專業】之間的關係就是這樣,同樣還有例如【總公司-分公司/部門】、【書包-書】,軟體開發中也是啊,【資料夾-檔案】、【容器-元件】

但是其實可以發現其共性,都是大範圍包括小範圍這樣的形式,例如每一個學院下面都有不同的專業,例如計算機學院下的軟體工程專業,我們可以將其稱之為 “整體-部分” 的模式

如果我們按照最簡單能想到的方式,或許就是多層繼承,例如 XXX大學,下面計算機學院等多個學院去繼承 XXX 大學,而軟體工程,電腦科學與技術等專業又去繼承了計算機學院

年輕人,這好嗎,這不好。

這種方式其實是按照組織的規模大小來進行劃分的,但我們從實際出發,除了其規模,我們更傾向於展示其組成結構,例如計算機學院下有多個專業,同時對其進行一定的維護業務,例如更新遍歷等

而使用今天要講的組合模式就可以實現我們的想法

二 分公司不就是一部門嗎

這個案例的需求是這樣的,一家在全國都有分銷機構的大公司,想要在全國的分公司中一起使用同一套辦公管理系統,例如在北京由總部,全國幾大城市有分公司,還有幾個省會有辦事處。要求總公司的人力資源部沒財務部等辦公管理功能在所有分公司和辦事處都需要有,該如何辦?

(一) 分析

首先捋一下總公司,分公司,辦事處,以及各自所屬幾個部門的關係

根據圖可以看出,北京公司總部是最高階別的,其擁有兩個最直接的部門,即:人力資源部和財務部,而分公司其實和這幾個部門是屬於同一級的,而人力資源部,財務部這兩個管理功能又可以複用於分公司。辦事處這個級別在下面一層,也是如此。

分析完這個圖,其實如何去做已經有了一個簡單的思路了,簡單的平行管理肯定是不合適的,所以我們將其做成一個樹狀結構,這樣維護管理起來也會非常方便

北京總公司就好比這個根節點,其下屬分公司也就是這棵樹的分支,像辦事處就是更小的分支,無論是總公司還是分公司,亦或辦事處的相關職能部門(如財務部)都沒有分支了,所以也就是葉子節點

直接講解例子或許會有一些不明朗各個公司,部門之間的關係,我們先通過演示最簡單的組合模式來鋪墊一下,下一個點,再來實現上面的公司例子

(二) 組合模式

(1) 什麼是組合模式

定義:組合模式有時又叫作“整體-部分”模式,它是一種將物件組合成樹狀的層次結構的模式,用來表示“整體-部分”的關係,使使用者對單個物件和組合物件具有一致的訪問性

(2) 結構圖

(3) 簡單程式碼

Component 為組合中的樹枝以及樹葉物件宣告公共介面

public abstract class Component {
    protected String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    // 新增部件
    public abstract void add(Component component);

    // 刪除部件
    public abstract void remove(Component component);

    // 遍歷所有子部件內容
    public abstract void traverseChild();
}

Leaf 即表示葉節點,起沒有子節點,所有 add 和 remove 方法均沒有具體業務,只是拋一個異常,只單純實現遍歷方法

public class Leaf extends Component{
    @Override
    public void add(Component component) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void remove(Component component) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void traverseChild() {
        System.out.println("執行:" + super.getName());
    }
}

Composite 代表樹枝,用來儲存子部件,主要作用來儲存和管理子部件

public class Composite extends Component {

    // 用來儲存組合的部件
    List<Component> list = new ArrayList<Component>();

    @Override
    public void add(Component component) {
        list.add(component);
    }

    @Override
    public void remove(Component component) {
        list.remove(component);
    }

    @Override
    public void traverseChild() {
        System.out.println("執行:" + name);
        for (Component c : list) {
            c.traverseChild();
        }
    }
}

測試一下

public class Test {
    public static void main(String[] args) {
        // 設定根節點
        Composite root = new Composite();
        root.name = "根節點";

        // 新增葉子節點
        Leaf leafA = new Leaf();
        leafA.setName("葉子節點 A");
        Leaf leafB = new Leaf();
        leafB.setName("葉子節點 B");
        // 將 A 和 B 新增到 root 下
        root.add(leafA);
        root.add(leafB);
        // 遍歷
        root.traverseChild();
    }
}

結果:

執行:根節點
執行:葉子節點 A
執行:葉子節點 B

(4) 透明方式和安全方式

在上面的程式碼中,我們在抽象的 Component 中定義了 add 和 remove 方法

// 新增部件
public abstract void add(Component component);
// 刪除部件
public abstract void remove(Component component);

這也就意味著,即使在葉子節點 Leaf 類中也需要實現 add 以及 remove 方法,(這裡我們做了拋異常處理,還可以選擇空實現)其實這種方式就叫做透明方式,也就是在 Component 中宣告所有用來管理子物件的方法,其中包括 add 以及 remove 方法,這樣但凡繼承或者實現這個類的子類都具備了 這些方法,這樣的行為好處在於,使得葉節點,枝節點這一件具有了完全一致的行為介面,缺點就是,葉節點中的空實現等並無意義

而安全方式就是不在 Component 中宣告 add 以及 remove 方法,而是在 Composite 宣告所有用來管理子類物件的方法,這樣就不會有剛才的問題了,其缺點是葉節點,枝節點不再具有相同的結構,客戶端呼叫需要增加一些判斷

(5) 優缺點

優點:

  • 類似例子中職能部門這種基本物件以及分公司辦事處等組合物件之間可以組合成更復雜的組合物件,而組合物件又可以被組合
  • 客戶端程式碼可以一致地處理單個物件和組合物件,無須關心自己處理的是單個物件,還是組合物件,客戶端呼叫方便
  • 組合體中加入新內容,不需要修改原始碼,滿足開閉原則

缺點:

  • 設計較複雜,類與類之間的層次關係需要捋清楚

(三) 公司示例程式碼實現

下面我們再結合上面具體的例子來應用一下組合模式(透明方式)

公司的抽象類,相當於上面的 Component

/**
 * 公司抽象類
 */
public abstract class Company {
    protected String name;

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

    // 增加
    public abstract void add(Company company);

    // 刪除
    public abstract void remove(Company company);

    // 顯示
    public abstract void display(int depth);

    // 履行職責
    public abstract void lineOfDuty();
}

具體公司類,相當於 Composite ,為了在控制檯輸出時能看出其層級結構,我根據當前節點的深度,增加了一下等於符號 MyStringUtil.repeatString("=", depth) 就是自己封裝了一個方法,根據 depth 的值來多次輸出一個字串

/**
 * 具體公司類,樹枝節點
 */
public class ConcreteCompany extends Company {
    // 用來儲存其下廚枝節點和葉節點
    private List<Company> children = new ArrayList<>();

    public ConcreteCompany(String name) {
        super(name);
    }

    @Override
    public void add(Company company) {
        children.add(company);
    }

    @Override
    public void remove(Company company) {
        children.remove(company);
    }

    @Override
    public void display(int depth) {
        // 顯示其枝節點名稱,並對其下級進行遍歷
        System.out.println(MyStringUtil.repeatString("=", depth) + name);
        for (Company c : children) {
            c.display(depth + 4);
        }
    }

    @Override
    public void lineOfDuty() {
        // 遍歷每一個孩子的節點
        for (Company c : children) {
            c.lineOfDuty();
        }
    }
}

上面用到的工具類就這樣

public class MyStringUtil {
    public static String repeatString(String repeatStr, int repeatNum) {
        StringBuilder stringBuilder = new StringBuilder();
        while (repeatNum-- > 0) {
            stringBuilder.append(repeatStr);
        }
        return stringBuilder.toString();
    }
}

下面是兩個具體的部門,也就是葉子節點

/**
 * 人力資源部
 */
public class HRDepartment extends Company {


    public HRDepartment(String name) {
        super(name);
    }

    @Override
    public void add(Company company) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void remove(Company company) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void display(int depth) {

        // 顯示其枝節點名稱,並對其下級進行遍歷
        System.out.println(MyStringUtil.repeatString("=", depth) + name);
    }

    @Override
    public void lineOfDuty() {
        System.out.println("【" + name + "】員工培訓管理");
    }
}
/**
 * 財務部
 */
public class FinanceDepartment extends Company {


    public FinanceDepartment(String name) {
        super(name);
    }

    @Override
    public void add(Company company) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void remove(Company company) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void display(int depth) {


        // 顯示其枝節點名稱,並對其下級進行遍歷
        System.out.println(MyStringUtil.repeatString("=", depth) + name);
    }


    @Override
    public void lineOfDuty() {
        System.out.println("【" + name + "】公司財務收支管理");
    }
}

測試類,我們分別建立了 root、comp、comp1 等多個 ConcreteCompany,通過 add 方法先給每部分都新增了這幾個職能部門,在通過像 root.add(comp); 這樣的程式碼表示了其層級

public class Test {
    public static void main(String[] args) {
        // 建立根節點
        ConcreteCompany root = new ConcreteCompany("北京總公司");
        root.add(new HRDepartment("總公司人力資源部"));
        root.add(new FinanceDepartment("總公司財務部"));

        ConcreteCompany comp = new ConcreteCompany("上海華東分公司");
        comp.add(new HRDepartment("華東分公司人力資源部"));
        comp.add(new FinanceDepartment("華東分公司財務部"));

        root.add(comp);
		
        ConcreteCompany comp = new ConcreteCompany("南京辦事處");
        comp1.add(new HRDepartment("南京辦事處人力資源部"));
        comp1.add(new FinanceDepartment("南京辦事處財務部"));
        comp.add(comp1);

        System.out.println("結構圖:");
        root.display(1);

        System.out.println("職責:");
        root.lineOfDuty();
    }
}

執行結果:

結構圖:
=北京總公司
=====總公司人力資源部
=====總公司財務部
=====上海華東分公司
=========華東分公司人力資源部
=========華東分公司財務部
=========南京辦事處
=============南京辦事處人力資源部
=============南京辦事處財務部
職責:
【總公司人力資源部】員工培訓管理
【總公司財務部】公司財務收支管理
【華東分公司人力資源部】員工培訓管理
【華東分公司財務部】公司財務收支管理
【南京辦事處人力資源部】員工培訓管理
【南京辦事處財務部】公司財務收支管理

相關文章