設計模式-組合模式


定義

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

示例

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

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

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等。

優缺點

優點

  • 客戶端調用簡單,可以像處理簡單元素一樣來處理復雜元素,從而使得客戶程序與復雜元素的內部結構解耦。
  • 可以方便的在結構中增加或者移除對象。

缺點

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

總結

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

源碼鏈接


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM