設計模式-組合模式

煮詩君發表於2020-09-04

定義

將物件組合成樹形結構以表示“部分-整體”的層次結構。組合模式使得對單個物件和組合物件的使用具有一致性。

示例

如下圖所示,就是日常工作中一個很常見的樹形結構的例子:

對於這種資料,我們通常會以類似如下二維關係表的形式儲存在資料庫中,他們之間的樹形結構關係由主外來鍵保持:

Id Name ParentId
1 音樂 0
2 知識 0
3 生活 0
4 科學科普 2
5 社科人文 2

但是在介面渲染的時候,這種自依賴的二維表結構就顯得不那麼人性化了,而組合模式主要就是為了將這種資料以樹形結構展示給客戶端,並且使得客戶端對每一個節點的操作都是一樣的簡單。

UML類圖

我們先看看組合模式的類圖:

  • Component:組合中的物件宣告介面,並實現所有類共有介面的預設行為。
  • Leaf:葉子結點,沒有子結點。
  • Composite:枝幹節點,用來儲存管理子節點,如增加和刪除等。

從類圖上可以看出,它其實就是一個普通的樹的資料結構,封裝的是對樹節點的增刪改查操作,因此,組合模式也是一種資料結構模式。

程式碼實現

組合模式理解起來比較簡單,我們直接看看程式碼如何實現。

透明模式

public abstract class Component
{
    public string Name { get; set; }

    public Component(string name)
    {
        this.Name = name;
    }

    public abstract int SumArticleCount();

    public abstract void Add(Component component);
    public abstract void Remove(Component component);

    public abstract void Display(int depth);
}

public class Composite : Component
{
    private List<Component> _components = new List<Component>();

    public Composite(string name):base(name)
    {

    }
    public override void Add(Component component)
    {
        _components.Add(component);
    }

    public override void Remove(Component component)
    {
        _components.Remove(component);
    }

    public override void Display(int depth)
    {
        Console.WriteLine(new string('-', depth) + Name);
        foreach (Component component in _components)
        {
            component.Display(depth + 1);
        }
    }

    public override int SumArticleCount()
    {
        int count = 0;
        foreach (var item in _components)
        {
            count += item.SumArticleCount();
        }
        return count;
    }
}

public class Leaf : Component
{
    public Leaf(string name) : base(name)
    {

    }

    public override void Add(Component component)
    {
        throw new InvalidOperationException("葉子節點不能新增元素");
    }

    public override void Remove(Component component)
    {
        throw new InvalidOperationException("葉子節點不能刪除元素");
    }

    public override int SumArticleCount()
    {
        return 1;
    }

    public override void Display(int depth)
    {
        Console.WriteLine(new string('-', depth) + Name);
    }
}

值得注意的是,由於Leaf也繼承了Component,因此必須實現父類中的所有抽象方法,包括Add()Remove(),但是我們知道,葉子節點是不應該有這兩個方法的,因此,只能給出一個空實現,或者丟擲一個非法操作的異常(建議丟擲異常,這樣可以明確的告訴呼叫者不能使用,空實現會對呼叫者造成困擾)。對於其他業務方法,葉子節點直接返回當前葉子的資訊,而枝幹節點採用遞迴的方式管理所有節點(其實組合模式的核心思想就是樹形結構+遞迴)。由於葉子節點和枝幹節點是繼承了父類完全相同的結構,因此,客戶端對整個樹形結構的所有節點具有一致的操作,不用關心具體操作的是葉子節點還是枝幹節點,因此,這種模式被叫做透明模式。客戶端呼叫程式碼如下:

static void Main(string[] args)
{
    Component root = new Composite("目錄");

    Component music = new Composite("音樂");
    Component knowledge = new Composite("知識");
    Component life = new Composite("生活");
    root.Add(music);
    root.Add(knowledge);
    root.Add(life);

    Component science = new Composite("科學科普");
    Component tech = new Composite("野生技術協會");
    knowledge.Add(science);
    knowledge.Add(tech);

    Component scienceArticle1 = new Leaf("科學科普文章1");
    Component scienceArticle2 = new Leaf("科學科普文章2");
    science.Add(scienceArticle1);
    science.Add(scienceArticle2);

    Component shoot = new Composite("攝影");
    Component program = new Composite("程式設計");
    Component english = new Composite("英語");
    tech.Add(shoot);
    tech.Add(program);
    tech.Add(english);

    Component shootArticle1 = new Leaf("攝影文章1");
    Component lifeArticle1 = new Leaf("生活文章1");
    Component lifeArticle2 = new Leaf("生活文章2");
    shoot.Add(shootArticle1);
    life.Add(lifeArticle1);
    life.Add(lifeArticle2);

    tech.Remove(program);
    knowledge.Display(0);
    Console.WriteLine("文章數:"+ knowledge.SumArticleCount());
}

透明模式是把組合使用的方法放到抽象類中,使得葉子物件和枝幹物件具有相同的結構,客戶端呼叫時具備完全一致的行為介面。但因為Leaf類本身不具備Add()Remove()方法的功能,所以實現它是沒有意義的,違背了單一職責原則和里氏替換原則。

安全模式

基於上面的問題,我們可以對實現進行改造,程式碼如下:

public abstract class Component
{
    public string Name { get; set; }

    public Component(string name)
    {
        this.Name = name;
    }

    public abstract int SumArticleCount();

    public abstract void Display(int depth);
}

public class Composite : Component
{
    private List<Component> _components = new List<Component>();

    public Composite(string name):base(name)
    {

    }
    public void Add(Component component)
    {
        _components.Add(component);
    }

    public void Remove(Component component)
    {
        _components.Remove(component);
    }

    public override void Display(int depth)
    {
        Console.WriteLine(new string('-', depth) + Name);
        foreach (Component component in _components)
        {
            component.Display(depth + 1);
        }
    }

    public override int SumArticleCount()
    {
        int count = 0;
        foreach (var item in _components)
        {
            count += item.SumArticleCount();
        }
        return count;
    }
}

public class Leaf : Component
{
    public Leaf(string name) : base(name)
    {

    }

    public override int SumArticleCount()
    {
        return 1;
    }

    public override void Display(int depth)
    {
        Console.WriteLine(new string('-', depth) + Name);
    }
}

我們去掉了父類中抽象的Add()Remove()方法,讓其獨立的被Composite控制,這樣Leaf中就不需要實現無意義的Add()Remove()方法了,使得對葉子節點的操作更加安全(不存在無意義的方法),因此,這種模式也叫安全模式。

安全模式是把枝幹和葉子節點區分開來,枝幹單獨擁有用來組合的方法,這種方法比較安全。但枝幹和葉子節點不具有相同的介面,客戶端的呼叫需要做相應的判斷,違背了依賴倒置原則。

由於這兩種模式各有優缺點,因此,無法斷定哪一種更優,選用哪一種方式還得取決於具體的需求。不過個人還是比較傾向於透明模式,因為這種模式,客戶端的呼叫更容易,況且,在軟體開發過程中,葉子也並沒有那麼容易識別,葉子不一定永遠都是葉子,例如,我們以為文章就是葉子,殊不知,當需求發生變化時,文章下面還可能有章節,這時透明模式也不失為一種預留擴充套件的手段。

應用例項

在實際工作中,這種樹形結構也是非常多見的,其中或多或少都體現了組合模式的思想,例如,檔案系統中的檔案與資料夾、Winform中的簡單控制元件與容器控制元件、XML中的Node和Element等。

優缺點

優點

  • 客戶端呼叫簡單,可以像處理簡單元素一樣來處理複雜元素,從而使得客戶程式與複雜元素的內部結構解耦。
  • 可以方便的在結構中增加或者移除物件。

缺點

客戶端需要花更多時間理清類之間的層次關係。這個通過上面客戶端的呼叫程式碼也可以看得出來,但是,任何設計都是在各種利弊之間做出權衡,例如,我們都知道通過二叉樹的二分查詢可以加快查詢速度,但是,它的前提是必須先構建二叉樹並且排好序。這裡也是一樣的,為了後期使用方便,前期構造的麻煩也是在所難免的。

總結

組合模式適用於處理樹形結構關係的場景,因此很好識別,但是並非所有樹形結構出現的場合都可以使用組合模式,例如,我們在寫業務介面的時候就大量存在樹形結構的關係,但我相信幾乎不會有人使用組合模式來組織這種關係然後再返回給客戶端,而是直接採用主外來鍵的方式組織,這是因為這種場合組合模式就已經不適用了。組合模式通常還是更適用於人機互動的場景,例如頁面佈局控制元件中。

原始碼連結

相關文章