定義
將物件組合成樹形結構以表示“部分-整體”的層次結構,組合模式使得對單個物件和組合物件的使用具有一致性,
示例
如下圖所示,就是日常作業中一個很常見的樹形結構的例子:

對于這種資料,我們通常會以類似如下二維關系表的形式存盤在資料庫中,他們之間的樹形結構關系由主外鍵保持:
| 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等,
優缺點
優點
- 客戶端呼叫簡單,可以像處理簡單元素一樣來處理復雜元素,從而使得客戶程式與復雜元素的內部結構解耦,
- 可以方便的在結構中增加或者移除物件,
缺點
客戶端需要花更多時間理清類之間的層次關系,這個通過上面客戶端的呼叫代碼也可以看得出來,但是,任何設計都是在各種利弊之間做出權衡,例如,我們都知道通過二叉樹的二分查找可以加快查詢速度,但是,它的前提是必須先構建二叉樹并且排好序,這里也是一樣的,為了后期使用方便,前期構造的麻煩也是在所難免的,
總結
組合模式適用于處理樹形結構關系的場景,因此很好識別,但是并非所有樹形結構出現的場合都可以使用組合模式,例如,我們在寫業務介面的時候就大量存在樹形結構的關系,但我相信幾乎不會有人使用組合模式來組織這種關系然后再回傳給客戶端,而是直接采用主外鍵的方式組織,這是因為這種場合組合模式就已經不適用了,組合模式通常還是更適用于人機互動的場景,例如頁面布局控制元件中,
原始碼鏈接
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/1060.html
標籤:設計模式
下一篇:行為型模式之責任鏈模式
