1. 程式人生 > 實用技巧 >【設計模式(八)】結構型模式之組合模式

【設計模式(八)】結構型模式之組合模式

個人學習筆記分享,當前能力有限,請勿貶低,菜鳥互學,大佬繞道

如有勘誤,歡迎指出和討論,本文後期也會進行修正和補充


前言

這個是我隨手截的檔案目錄,這樣的結構都很眼熟吧?

一個個檔案,組成一個資料夾,檔案和資料夾又可以組成更大的資料夾,進而形成一個樹形結構

那麼,我們在點開的時候,需要先確認目標是資料夾或者檔案,檔案就開啟,資料夾則是展開下一級

也就是說我們對於"部分"與“整體”採取了不同的方案,但是這樣帶來了一些不必要的麻煩,我們只想開啟這個目標,具體怎麼開啟那是你們自己的事情

進而言之,客戶只關心對目標進行操作,並不關心因為目標不同而導致的差異。使用者對於目標的“部分”與“整體”是一致對待的。

比如,刪除目標,客戶只需要點選刪除即可,並不關心具體邏輯,實際上如果是檔案就直接刪除,如果是資料夾需要先刪除下一層級的所有目標。

複製、貼上、移動這類操作同理

換個例子,我們告訴一個部門明天放假,只需要告知負責人即可

至於這個部門,包括一整個公司分部,還是研發部,還是研發小組,還是說就負責人一個人,我麼並不需要關心,這是負責人該關心的事情

事實上如果這個部門不止負責人一個人的話,他大概率也是轉告下一層部門的負責人而已(套娃??)

因此,你看,我們告知一個人,或者告知任何一個部門,都是一樣的,並不需要先確定是哪層的負責人


這就是組合模式,又稱部分整體模式,用於將一組相似的物件作為單一的物件整體,進而將部分與整體構造成樹形結構。

這種模式建立了一個包含自己物件組的類,並提供操作相同物件組的方式。

組合模式定義瞭如何將容器物件和葉子物件進行遞迴組合,使得客戶在使用的過程中無須進行區分,可以對他們進行一致的處理


1.介紹

使用目的:將物件組合成樹形結構以表示"部分-整體"的層次結構,進而使客戶可能夠對單體物件或者組合物件的使用具有一致性

使用時機:希望使用者能夠忽略組合物件與單個物件的區別,進行統一的處理

解決問題:將“部分”與“整體”區別對待會帶來不必要的麻煩

實現方法:將容器物件和葉子物件進行遞迴組合,使得客戶在使用的過程中無須進行區分,可以對他們進行一致的處理

應用例項:

  • 對於檔案/資料夾的刪除、複製、剪下、貼上、移動等操作
  • 向一個個人/部門傳遞訊息或者指令,只需要告知負責人即可
  • 需求展示一個無限層級的目錄,如圖書管理系統(曾經遇到的需求,層級未知,最後乾脆做成了無限層級)

優點

  1. 客戶對於“部分”和“整體”的操作具有一致性,無疑提高了使用者體驗
  2. 高層程式碼呼叫簡單方便,也簡化了客戶端程式碼
  3. 節點自由度增加,可以選擇僅變更自己,或變更所有子節點
  4. 組合內部增加新的節點很方便,不需要修改結構的原始碼,滿足“開閉原則”

缺點

  1. 所有節點都是實現類,而不是介面,違背了依賴倒置原則
  2. 設計較為複雜,需要理清不同層級之間的關係
  3. 難以使用整合的方法進行擴充套件

2.結構

主要包含3個角色

  • 抽象構件(Component)角色:宣告樹枝節點和葉子節點的公共介面,並實現預設行為。

    根據是否宣告訪問和管理子類的介面,分為透明模式和安全模式

  • 樹葉構件(Leaf)角色:組合中的葉節點物件,它沒有子節點,用於實現抽象構件角色中宣告的公共介面。

  • 樹枝構件(Composite)角色:組合中的分支節點物件,它有子節點。它實現了抽象構件角色中宣告的介面,同時還需要儲存和管理子節點。

其實也可以將三者融為一體,一個類就搞定了,但是不利於擴充套件,功能較少的時候可以這樣做

圖就不花了,這套娃的結構根本無從下手


3.實現

這裡給出三種示例,分別是簡單組合模式、透明模式和安全模式

3.1.簡單組合模式

不利於擴充套件,但程式碼簡單,適用於功能較少的機構

之所以列出來說,是因為這種其實才是最常見的,尤其是演算法中經常用到,包括連結串列也是使用的這種結構

3.1.1.示例1

模擬業務如下:

  • 連結串列由節點連線構成
  • 每個節點儲存一個值和下一個節點,下一個節點可能為空

程式碼很簡單直接貼了,經常刷演算法題的都能默寫下來了

package com.company.test.composite;

import lombok.Data;

@Data
class Node {
    public int val;
    public Node next;

    public Node(int val, Node next) {
        this.val = val;
        this.next = next;
    }

    public void show() {
        if (next == null) {
            //為最後一個節點,列印本身的值並轉行
            System.out.println(val);
        } else {
            //不為最後一個節點,列印本身的值,並列印下一個節點
            System.out.print(val + " -> ");
            next.show();
        }
    }
}

public class SimpleCompositeTest {
    public static void main(String[] args) {
        Node node1 = new Node(1, null);
        Node node2 = new Node(2, node1);
        Node node3 = new Node(3, node2);
        Node node4 = new Node(4, node3);

        node1.show();
        node2.show();
        node3.show();
        node4.show();
    }
}

執行結果

我們在這裡捨棄了抽象構建,而且樹葉構件和樹枝構件使用同一個類實現即可,通過next是否為空判斷是否是葉子節點


3.1.2.示例2

模擬業務如下

  • 職員資訊包括4個數據:姓名、職位、薪水、下級人員
  • 職員資訊提供介面進行列印,可以列印當前職員資訊,也可以同時列印所有下級人員資訊

結構與示例1類似,故不多做解釋

package com.company.test.composite;

import lombok.Data;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@Data
class Employee {
    private String name;
    private String dept;
    private int salary;

    private List<Employee> subordinate;

    public Employee(String name, String dept, int salary, List<Employee> subordinate) {
        this.name = name;
        this.dept = dept;
        this.salary = salary;
        this.subordinate = subordinate;
    }

    public String toString() {
        String subordinateNames = subordinate.stream().map(Employee::getName).collect(Collectors.joining(", "));
        return ("Employee :[ "
                + "Name : " + name
                + ", dept : " + dept
                + ", salary : " + salary
                + ", subordinate : " + subordinateNames
                + " ]");
    }

    public void showCurrent() {
        System.out.println(this.toString());
    }

    public void showAll() {
        showCurrent();
        subordinate.forEach((m) -> {
            m.showAll();
        });
    }
}

public class SimpleCompositeTest1 {
    public static void main(String[] args) {
        Employee clerk1 = new Employee("clerk1", "clerk", 10000, new ArrayList<>());
        Employee clerk2 = new Employee("clerk2", "clerk", 10000, new ArrayList<>());

        Employee manager1 = new Employee("manager1", "manager", 50000, Arrays.asList(new Employee[]{clerk1, clerk2}));
        Employee manager2 = new Employee("manager2", "manager", 50000, new ArrayList<>());

        Employee ceo = new Employee("Jobs", "ceo", 150000, Arrays.asList(new Employee[]{manager1, manager2}));

        ceo.showAll();
    }
}

執行結果


3.2.透明組合模式

透明模式是把組合使用的方法放到抽象類中,不管葉子物件還是樹枝物件都有相同的結構

這樣做的好處就是葉子節點和樹枝節點對於外界沒有區別,它們具備完全一致的行為介面。

但因為Leaf類本身不具備add()、remove()方法的功能,所以實現它是沒有意義的

  1. 定義抽象構件角色Component

    abstract class Component {
        protected String name;
    
        public Component(String name) {
            this.name = name;
        }
    
        public abstract void add(Component component);
    
        public abstract void remove(Component component);
    
        public abstract List<Component> getChildren();
    
        public abstract void show(int depth);
    }
    
  2. 定義樹葉構件Leaf

    class Leaf extends Component {
    
        public Leaf(String name) {
            super(name);
        }
    
        @Override
        public void add(Component component) {
            //空實現,丟擲“不支援請求”異常
            throw new UnsupportedOperationException();
        }
    
        @Override
        public void remove(Component component) {
            //空實現,丟擲“不支援請求”異常
            throw new UnsupportedOperationException();
        }
    
        @Override
        public List<Component> getChildren() {
            return null;
        }
    
        @Override
        public void show(int depth) {
            while (depth-- > 0) {
                System.out.print("-");
            }
            System.out.println(name);
        }
    }
    
  3. 定義樹枝構件Composite

    class Composite extends Component {
    
        List<Component> children = new ArrayList<>();
    
        public Composite(String name) {
            super(name);
        }
    
        @Override
        public void add(Component component) {
            children.add(component);
        }
    
        @Override
        public void remove(Component component) {
            children.remove(component);
        }
    
        @Override
        public List<Component> getChildren() {
            return children;
        }
    
        @Override
        public void show(int depth) {
            int nowDepth = depth;
            while (depth-- > 0) {
                System.out.print("-");
            }
            System.out.println(name);
    
            children.forEach(m -> {
                m.show(nowDepth + 1);
            });
        }
    }
    

完整程式碼

package com.company.test.composite;

import java.util.ArrayList;
import java.util.List;

abstract class Component {
    protected String name;

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

    public abstract void add(Component component);

    public abstract void remove(Component component);

    public abstract List<Component> getChildren();

    public abstract void show(int depth);
}

class Leaf extends Component {

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

    @Override
    public void add(Component component) {
        //空實現,丟擲“不支援請求”異常
        throw new UnsupportedOperationException();
    }

    @Override
    public void remove(Component component) {
        //空實現,丟擲“不支援請求”異常
        throw new UnsupportedOperationException();
    }

    @Override
    public List<Component> getChildren() {
        return null;
    }

    @Override
    public void show(int depth) {
        while (depth-- > 0) {
            System.out.print("-");
        }
        System.out.println(name);
    }
}

class Composite extends Component {

    List<Component> children = new ArrayList<>();

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

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

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

    @Override
    public List<Component> getChildren() {
        return children;
    }

    @Override
    public void show(int depth) {
        int nowDepth = depth;
        while (depth-- > 0) {
            System.out.print("-");
        }
        System.out.println(name);

        children.forEach(m -> {
            m.show(nowDepth + 1);
        });
    }
}

public class ClearCompositeTest {
    public static void main(String[] args) {
        Component leaf1 = new Leaf("leaf1");
        Component leaf2 = new Leaf("leaf2");

        Component composite1=new Composite("composite1");
        composite1.add(leaf1);
        composite1.add(leaf2);

        Component leaf3 = new Leaf("leaf3");

        Component composite3=new Composite("composite3");
        composite3.add(composite1);
        composite3.add(leaf3);

        composite3.show(1);
    }
}

執行結果

如圖,組裝了一個目錄,並將其打印出來

可以看到,樹葉和樹枝擁有同樣的功能,但樹葉的部分功能並沒有正常執行(丟擲異常或空實現),這樣會帶來安全性問題

安全組合模式就是為了解決這種情況


3.3.安全組合模式

在該方式中,將管理子構件的方法移到樹枝構件中,抽象構件和樹葉構件沒有對子物件的管理方法

這樣就避免了上一種方式的安全性問題,但由於葉子和分支有不同的介面,客戶端在呼叫時要知道樹葉物件和樹枝物件的存在,所以失去了透明性

結構一樣的,就直接貼程式碼了,自己對比一下

package com.company.test.composite;

import java.util.ArrayList;
import java.util.List;

abstract class Component {
    protected String name;

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

    public abstract void add(Component component);

    public abstract void remove(Component component);

    public abstract List<Component> getChildren();

    public abstract void show(int depth);
}

class Leaf extends Component {

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

    @Override
    public void add(Component component) {
        //空實現,丟擲“不支援請求”異常
        throw new UnsupportedOperationException();
    }

    @Override
    public void remove(Component component) {
        //空實現,丟擲“不支援請求”異常
        throw new UnsupportedOperationException();
    }

    @Override
    public List<Component> getChildren() {
        return null;
    }

    @Override
    public void show(int depth) {
        while (depth-- > 0) {
            System.out.print("-");
        }
        System.out.println(name);
    }
}

class Composite extends Component {

    List<Component> children = new ArrayList<>();

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

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

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

    @Override
    public List<Component> getChildren() {
        return children;
    }

    @Override
    public void show(int depth) {
        int nowDepth = depth;
        while (depth-- > 0) {
            System.out.print("-");
        }
        System.out.println(name);

        children.forEach(m -> {
            m.show(nowDepth + 1);
        });
    }
}

public class ClearCompositeTest {
    public static void main(String[] args) {
        Component leaf1 = new Leaf("leaf1");
        Component leaf2 = new Leaf("leaf2");

        Component composite1=new Composite("composite1");
        composite1.add(leaf1);
        composite1.add(leaf2);

        Component leaf3 = new Leaf("leaf3");

        Component composite3=new Composite("composite3");
        composite3.add(composite1);
        composite3.add(leaf3);

        composite3.show(1);
    }
}

執行結果

4.透明組合模式與安全組合模式的區別

透明模式:

  • 只需要在定義的時候確定是樹葉或者樹枝,使用的時候樹葉和樹枝可以當做同一個物件使用
  • 樹葉實現了所有功能,但部分功能實際上並不擁有,需要丟擲異常或者空實現,會帶來安全性問題

安全模式

  • 使用時需要知道是樹葉或者樹枝,部分功能可能存在差異
  • 所有功能都正常實現了,所以不會帶來透明模式的安全性問題
  • 因為需要知道是節點型別,使用不便,一定程度上違背了初衷

簡單點說,一種是葉節點與樹枝節點具備一致的行為介面但有空實現的透明模式,另一種是樹枝節點單獨擁有用來組合的方法但呼叫不便的安全模式

使用哪種,自行取捨咯,如果是圖方便,簡單組合模式就可以滿足很多需求,如果需要保證安全,就需要使用安全組合模式,但是最符合初衷的應該是透明組合模式


5.擴充套件使用

5.1.將節點進一步抽象化

模擬資料夾目錄,包括資料夾和檔案

package com.company.test.composite;

import lombok.Data;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Data
abstract class Files {
    protected String name;

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

    public abstract void check();

    public abstract void copyFiles();
}

abstract class File extends Files {
    public File(String name) {
        super(name);
    }

    @Override
    public void copyFiles() {
        System.out.println("copy file: " + name);
    }
}

class Text extends File {

    public Text(String text) {
        super(text);
    }

    @Override
    public void check() {
        System.out.println("show text: " + name);
    }
}

class Mp3 extends File {
    public Mp3(String name) {
        super(name);
    }

    @Override
    public void check() {
        System.out.println("play mp3: " + name);
    }

}

class Folder extends Files {
    public Folder(String name) {
        super(name);
    }

    List<Files> subordinateFiles = new ArrayList<>();

    public void addFiles(Files files) {
        subordinateFiles.add(files);
    }

    public void removeFiles(Files files) {
        subordinateFiles.remove(files);
    }

    @Override
    public void check() {
        String subordinateFileNames = subordinateFiles.stream().map(m -> m.getName()).collect(Collectors.joining(", "));
        System.out.println("open folder: " + name + ", subordinateFiles: " + subordinateFileNames);
    }

    @Override
    public void copyFiles() {
        subordinateFiles.forEach(m -> {
            m.copyFiles();
        });
        System.out.println("copy folder: " + name);
    }
}

public class FileCompositeTest {
    public static void main(String[] args) {
        Text text = new Text("HelloWorld.text");
        Mp3 mp3 = new Mp3("我在昨天的夢裡又看見了你.mp3");
        Text lyric = new Text("我在昨天的夢裡又看見了你.text");

        Folder folder = new Folder("我在昨天的夢裡又看見了你");
        folder.addFiles(mp3);
        folder.addFiles(lyric);

        Folder folder1 = new Folder("empty");

        Folder root = new Folder("root");
        root.addFiles(folder);
        root.addFiles(folder1);
        root.addFiles(text);

        System.out.println("<---------------------------操作資料夾:root------------------------------->");
        root.check();
        root.copyFiles();

        System.out.println("<---------------------------操作資料夾:我在昨天的夢裡又看見了你------------------------------->");
        folder.check();
        folder.copyFiles();

        System.out.println("<---------------------------操作檔案:我在昨天的夢裡又看見了你.mp3------------------------------->");
        mp3.check();
        mp3.copyFiles();
    }
}

執行結果

檔案目錄(模擬)

看,無論是mp3檔案,或者text檔案,或者folder資料夾,我們都可以執行同樣的check()copyFiles()操作


其餘擴充套件使用後續再追加,暫時只想到這裡就寫到這裡


後記

將相似的目標提取其共同點,從而可以進行部分一致性操作,而目標本身只需要關注自己的特點,將共同點交由介面或者父類處理

其實這也是多型和繼承的目的,所以從學習Java開始,我們其實就在按照這種思想設計程式,組合模式不過是其中一種方案而已


作者:Echo_Ye

WX:Echo_YeZ

Email :[email protected]

個人站點:在搭了在搭了。。。(右鍵 - 新建資料夾)