1. 程式人生 > 實用技巧 >設計模式學習筆記(十一):組合模式

設計模式學習筆記(十一):組合模式

1 概述

1.1 概述

對於樹形結構,比如檔案目錄,一個資料夾中可以包含多個資料夾和檔案,而一個檔案中不能在包含子檔案或者子資料夾,在這裡可以稱資料夾為容器,稱檔案為葉子

在樹形結構中,當容器物件(比如資料夾)的某個方法被呼叫時,將遍歷整個資料夾,尋找也包含這個方法的成員物件(容器物件或葉子物件)並呼叫執行。由於容器物件以及葉子物件在功能上的區別,使用這些物件的程式碼中必須有區別對待容器物件以及葉子物件,但大多數情況下需要一致性處理它們。

組合模式為解決此類問題而生,它可以讓葉子物件以及容器物件的使用具有一致性。

1.2 定義

組合模式:組合多個物件形成樹形結構以表示具有“整體-部分”關係的層次結構。組合模式對單個物件(葉子物件)和組合物件(容器物件)的使用具有一致性。


組合模式又叫“部分-整體”模式,它是一種物件結構型模式。

1.3 結構圖

1.4 角色

  • Component(抽象構件):可以是介面或者抽象類,為葉子構件和容器構件物件宣告介面,在該角色中可以包含所有子類共有行為的宣告和實現。在抽象構件中定義了訪問以及管理它的子構件的方法,例如增加/刪除/獲取子構件
  • Leaf(葉子構件):表示葉子節點物件,葉子節點沒有子節點,它實現了在抽象構件中定義的行為,對於訪問以及管理子構件的方法,通常會丟擲異常
  • Composite(容器構件):表示容器節點物件,容器節點包含子節點,其子節點可以是葉子節點,也可以是容器節點,它提供一個集合用於儲存子節點,實現了在抽象構件中定義的行為,包括訪問以及管理子構件的方法

2 典型實現

2.1 步驟

組合模式的關鍵是定義了一個抽象構件類,它既可以表示葉子也可以表示容器,客戶端針對該抽象構件進行程式設計,無須知道到底是葉子還是容器,同時容器物件與抽象構件之間需要建立一個聚合關聯關係,在容器物件中既可以包含葉子也可以包含容器,以此實現遞迴組合,形成樹形結構。

因此首先需要定義抽象構件類,通用步驟如下:

  • 定義抽象構件:定義抽象構件類,新增四個基本方法:增加/刪除/獲取成員+業務方法,可以將抽象構件類定義為抽象類或者介面
  • 定義葉子構件:繼承或實現抽象構件類,覆蓋或實現具體業務方法,同時對於管理或訪問子構件的方法提供異常處理或錯誤提示
  • 定義容器構件:繼承或實現抽象構件類,覆蓋或實現抽象構件中的所有方法,一般來說容器構件會包含一個集合私有成員用於儲存抽象構件,在業務方法中對這個集合進行遍歷從而實現遞迴呼叫

2.2 抽象構件

抽象構件一般定義如下:

abstract class Component
{
    abstract void add(Component c);
    abstract void remove(Component c);
    abstract Component getChild(int i);
    abstract void operation();
}

2.3 葉子構件

class Leaf extends Component
{
    public void add(Component c)
    {
        //葉子構件不能訪問該方法
        System.out.println("錯誤,不能訪問新增構件方法!");
    }

    public void remove(Component c)
    {
        //葉子構件不能訪問該方法
        System.out.println("錯誤,不能訪問刪除構件方法!");
    }

    public Component getChild(int i)
    {
        //葉子構件不能訪問該方法
        System.out.println("錯誤,不能訪問獲取構件方法!");
        return null;
    }

    public void operation()
    {
        System.out.println("葉子業務方法");
    }
}

葉子構件只需要覆蓋具體業務方法opeartion,對於管理子構件的方法可以提示錯誤或者丟擲異常來處理。

2.4 容器構件

class Composite extends Component
{
    private ArrayList<Component> list = new ArrayList<>();
    
    public void add(Component c)
    {
        list.add(c);
    }

    public void remove(Component c)
    {
        list.remove(c);
    }

    public Component getChild(int i)
    {
        return list.get(i);
    }

    public void operation()
    {
		list.forEach(Component::operation);
    }
}

容器構件只需要簡單實現管理子構件的方法,對於業務方法一般需要對抽象構件集合進行遍歷來實現遞迴呼叫。

3 例項

開發一個防毒軟體系統,可以對某個資料夾或單個檔案進行防毒,還能根據檔案型別的不同提供不同的防毒方式,比如文字檔案和影象檔案的防毒方式有所差異,使用組合模式對該系統進行設計。

首先定義抽象構件類AbstractFileFolder作為容器構件類,ImageFileTextFileVideoFile作為葉子構件類,程式碼如下:

public class Test
{
    public static void main(String[] args) {
        AbstractFile file1,file2,file3,file4,folder1,folder2;
        file1 = new ImageFile("影象檔案1號");
        file2 = new VideoFile("視訊檔案1號");
        file3 = new TextFile("文字檔案1號");
        file4 = new ImageFile("影象檔案2號");

        folder1 = new Folder("資料夾1");
        folder2 = new Folder("資料夾2");

        try
        {
            folder2.add(file1);
            folder2.add(file2);
            folder2.add(file3);
            folder1.add(file4);
            folder1.add(folder2);
        }
        catch(IllegalAccessException e)
        {
            e.printStackTrace();
        }
        
        folder1.killVirus();
        System.out.println();
        folder2.killVirus();
    }
}

//抽象構件類
abstract class AbstractFile
{
    protected String name;
    abstract void add(AbstractFile file) throws IllegalAccessException;
    abstract void remove(AbstractFile file) throws IllegalAccessException;
    abstract AbstractFile getChild(int i) throws IllegalAccessException;
    public void killVirus()
    {
        System.out.println(name+" 防毒");
    }
}

//葉子構件類
class ImageFile extends AbstractFile
{
    public ImageFile(String name)
    {
        this.name = name;
    }

    public void add(AbstractFile c)
    {
        throw new IllegalAccessException("錯誤,不能訪問新增檔案方法!");
    }

    public void remove(AbstractFile c)
    {
        throw new IllegalAccessException("錯誤,不能訪問刪除檔案方法!");
    }

    public AbstractFile getChild(int i)
    {
        throw new IllegalAccessException("錯誤,不能訪問獲取檔案方法!");
    }
}

//葉子構件類
class TextFile extends AbstractFile
{
    public TextFile(String name)
    {
        this.name = name;
    }

    public void add(AbstractFile c)
    {
        throw new IllegalAccessException("錯誤,不能訪問新增檔案方法!");
    }

    public void remove(AbstractFile c)
    {
        throw new IllegalAccessException("錯誤,不能訪問刪除檔案方法!");
    }

    public AbstractFile getChild(int i)
    {
        throw new IllegalAccessException("錯誤,不能訪問獲取檔案方法!");
    }
}

//葉子構件類
class VideoFile extends AbstractFile
{
    public VideoFile(String name)
    {
        this.name = name;
    }

	public void add(AbstractFile c)
    {
        throw new IllegalAccessException("錯誤,不能訪問新增檔案方法!");
    }

    public void remove(AbstractFile c)
    {
        throw new IllegalAccessException("錯誤,不能訪問刪除檔案方法!");
    }

    public AbstractFile getChild(int i)
    {
        throw new IllegalAccessException("錯誤,不能訪問獲取檔案方法!");
    }
}

//容器構件類
class Folder extends AbstractFile
{
    private ArrayList<AbstractFile> list = new ArrayList<>();
    public Folder(String name)
    {
        this.name = name;
    }

    public void add(AbstractFile c)
    {
        list.add(c);
    }

    public void remove(AbstractFile c)
    {
        list.remove(c);
    }

    public AbstractFile getChild(int i)
    {
        return list.get(i);
    }

    public void killVirus()
    {
        System.out.println("對 "+name+" 進行防毒");
        list.forEach(AbstractFile::killVirus);
    }
}

輸入如下:

4 透明組合模式與安全組合模式

4.1 如何簡化程式碼

儘管組合模式的擴充套件性好,在上面的例子中增加新的檔案型別無須修改原有程式碼,但是,由於抽象構件類AbstractFile聲明瞭與葉子構件無關的構件管理方法,因此 需要實現這些方法,這樣就會帶來很多重複性的工作。

解決方案有兩個:

  • 抽象構件提供預設實現:葉子構件中的構件管理方法轉移到抽象構件中提供預設實現
  • 抽象構件刪除方法:在抽象構件中不提供管理構件的方法

4.2 預設實現

如果使用抽象構件提供預設實現的方法,則上述例子程式碼簡化如下:

abstract class AbstractFile
{
    protected String name;
    public AbstractFile(String name)
    {
        this.name = name;
    }
    public void add(AbstractFile file) throws IllegalAccessException
    {
        throw new IllegalAccessException("錯誤,不能訪問新增檔案方法!");
    }
    public void remove(AbstractFile file) throws IllegalAccessException
    {
        throw new IllegalAccessException("錯誤,不能訪問刪除檔案方法!");
    }
    public AbstractFile getChild(int i) throws IllegalAccessException
    {
        throw new IllegalAccessException("錯誤,不能訪問獲取檔案方法!");
    }
    public void killVirus()
    {
        System.out.println(name+" 防毒");
    }
}

class ImageFile extends AbstractFile
{
    public ImageFile(String name)
    {
        super(name);
    }
}

class TextFile extends AbstractFile
{
    public TextFile(String name)
    {
        super(name);
    }
}

class VideoFile extends AbstractFile
{
    public VideoFile(String name)
    {
        super(name);
    }
}

在葉子構件中只有構造方法(實際上業務方法應該是抽象的,在葉子構件中實現業務方法,這裡的業務方法是killVirus(),這裡是進行了簡化),這樣修改雖然簡化了程式碼,但是總的來說為葉子構件提供這些方法是沒有意義的,因為葉子不會再下一個層次的物件,這在編譯階段不會出錯 ,但是在執行階段可能會出錯。

4.3 刪除方法

如果使用抽象構件刪除方法的方式進行簡化程式碼,則上述例子簡化如下:

abstract class AbstractFile
{
    protected String name;
    public AbstractFile(String name)
    {
        this.name = name;
    }
    abstract void killVirus();
}

class ImageFile extends AbstractFile
{
    public ImageFile(String name)
    {
        super(name);
    }
    public void killVirus()
    {
        System.out.println("影象檔案"+name+"防毒");
    }
}

class TextFile extends AbstractFile
{
    public TextFile(String name)
    {
        super(name);
    }
    public void killVirus()
    {
        System.out.println("文字檔案"+name+"防毒");
    }
}

class VideoFile extends AbstractFile
{
    public VideoFile(String name)
    {
        super(name);
    }
    public void killVirus()
    {
        System.out.println("視訊檔案"+name+"防毒");
    }
}

這樣做葉子構件就無法訪問管理構件的方法了,但是帶來的壞處是客戶端無法統一針對抽象構件類AbstractFile進行程式設計,修改之前程式碼如下:

AbstractFile file1,file2,file3,file4,folder1,folder2;

由於AbstractFile中刪除了管理構件方法,因此客戶端需要修改程式碼如下:

AbstractFile file1,file2,file3,file4;
Folder folder1,folder2;

4.4 透明組合模式

透明組合模式就是第一種解決方案中的方法,在抽象構件中宣告所有用於管理構件的方法,這樣做的好處是確保所有的構件類都具有相同的介面,客戶端可以針對抽象構件進行統一程式設計,結構圖如下:

透明組合模式的缺點是不夠安全,因為葉子物件和容器物件在本質上是有區別的。葉子物件不可能有下一層次的物件,提供管理構件的方法是沒有意義的,在編譯階段不會報錯,但是在執行階段可能會出錯。

4.5 安全組合模式

安全組合模式就是第二種方法的辦法,安全組合模式中,抽象構件沒有宣告管理構件的方法,而是在容器構件中新增管理構件的方法,這種做法是安全的因為葉子物件不可能呼叫到這些方法。結構圖如下:

安全組合模式的缺點是不夠透明,因為葉子構件與容器構件具有不同的方法,管理構件的方法在容器構件中定義,客戶端不能完全針對抽象構件進行程式設計,必須有區別地對待葉子構件與容器構件。

5 主要優點

  • 層次控制:組合模式可以清楚定義分層次的複雜物件,表示物件的全部或者部分層次,它讓客戶端忽略了層次的差異,方便對整個層次結構進行控制
  • 一致使用構件:客戶端可以一致地使用容器構件或者葉子構件,也就是能針對構件抽象層一致性程式設計
  • 擴充套件性好:增加新的容器構件或者葉子構件都很方便,符合開閉原則
  • 有效針對樹形結構:組合模式為樹形結構的面向物件實現提供了一種靈活的解決方案,通過葉子構件與容器構件的遞迴組合,可以形成複雜的樹形結構,但控制樹形結構卻很簡單

6 主要缺點

  • 難以限制構件型別:增加新構件時難以限制構件型別,比如希望容器構件中只有某一特定型別的葉子構件,例如一個只能包含圖片的資料夾,使用組合模式時不能依賴型別系統來施加這些約束,需要再執行時進行型別檢查來實現,過程較為複雜

7 適用場景

  • 具有整體和部分的層次結構中,希望通過一種方式忽略整體與部分的差異,客戶端可以一致性對待它們
  • 處理樹形結構
  • 系統中能夠分離出葉子構件以及容器構件,而且型別不固定,需要增加新的葉子構件或者容器構件

8 總結