引子:分公司不就是一個部門嗎?#
小菜公司接到客戶的辦公管理系統,總部有人力資源部、財務部、運營部等。但客戶想把同一套系統推廣到全國各地的分公司、辦事處:
- 北京總公司
- 人力資源部、財務部
- 上海華東分公司
- 人力資源部、財務部
- 南京辦事處、杭州辦事處
- 人力資源部、財務部
這是「部分與整體」的關係——分公司、辦事處與總公司是樹狀結構,不是平行的。
類似情境很多:
- 賣電腦:可以單獨配件,也可以組裝整機
- 複製文件:單檔複製,也可以整資料夾複製
- 文字編輯:單字加粗,也可整段加粗
組合模式#
組合模式(Composite Pattern):將物件組合成樹形結構以表示「部分-整體」的層次結構。組合模式使得用戶對單個物件和組合物件的使用具有一致性。[DP]
結構#
- Component:為組合中的物件聲明介面,並在適當情況下實現所有類別共有介面的預設行為;聲明用於訪問和管理子部件的介面
- Leaf(葉節點):在組合中表示葉節點物件,沒有子節點
- Composite(組合):定義有枝節點行為,用來存儲子部件,實現與子部件有關的操作(如
Add、Remove)
classDiagram
class Component {
<<abstract>>
+Add(Component)*
+Remove(Component)*
+Display(int)*
}
class Leaf {
+Display(int)
}
class Composite {
-List~Component~ children
+Add(Component)
+Remove(Component)
+Display(int)
}
Component <|-- Leaf
Component <|-- Composite
Composite o--> Component : childrenabstract class Component
{
protected string name;
public Component(string name) { this.name = name; }
public abstract void Add(Component c);
public abstract void Remove(Component c);
public abstract void Display(int depth);
}
class Leaf : Component
{
public Leaf(string name) : base(name) { }
public override void Add(Component c) { Console.WriteLine("Cannot add to a leaf"); }
public override void Remove(Component c) { Console.WriteLine("Cannot remove from a leaf"); }
public override void Display(int depth)
{
Console.WriteLine(new string('-', depth) + name);
}
}
class Composite : Component
{
private List<Component> children = new List<Component>();
public Composite(string name) : base(name) { }
public override void Add(Component c) => children.Add(c);
public override void Remove(Component c) => children.Remove(c);
public override void Display(int depth)
{
Console.WriteLine(new string('-', depth) + name);
foreach (var child in children)
child.Display(depth + 2);
}
}透明方式 vs. 安全方式#
透明方式#
把 Add、Remove 都聲明在 Component 中——所有子類都具備這些介面。
優點:葉節點與枝節點對外完全一致的介面,客戶端無需判斷型別。
缺點:
Leaf本身不需要Add、Remove,實作它們沒有意義(強制呼叫會出錯)。
安全方式#
把 Add、Remove 移到 Composite 才聲明,Leaf 不必實作。
優點:葉節點不會誤被呼叫
Add/Remove。缺點:兩者介面不再相同,客戶端要判斷類型——不夠透明。
兩者各有取捨,視情況決定。
何時使用?#
當需求中是體現部分與整體的層次結構,且希望用戶忽略組合物件與單個物件的不同、統一地使用結構中的所有物件時,就應該考慮組合模式。
實務範例:
- ASP.NET 的
TreeView控件 - 自訂控件:把基本控件組合成複合控件——所有 Web 控件的基類
System.Web.UI.Control都有Add、Remove方法
範例:公司管理系統#
abstract class Company
{
protected string name;
public Company(string name) { this.name = name; }
public abstract void Add(Company c);
public abstract void Remove(Company c);
public abstract void Display(int depth);
public abstract void LineOfDuty(); // 履行職責
}
class ConcreteCompany : Company // 樹枝節點
{
private List<Company> children = new List<Company>();
public ConcreteCompany(string name) : base(name) { }
public override void Add(Company c) => children.Add(c);
public override void Remove(Company c) => children.Remove(c);
public override void Display(int depth)
{
Console.WriteLine(new string('-', depth) + name);
foreach (var c in children) c.Display(depth + 2);
}
public override void LineOfDuty()
{
foreach (var c in children) c.LineOfDuty();
}
}
class HRDepartment : Company // 葉節點
{
public HRDepartment(string name) : base(name) { }
public override void Add(Company c) { }
public override void Remove(Company c) { }
public override void Display(int depth)
=> Console.WriteLine(new string('-', depth) + name);
public override void LineOfDuty()
=> Console.WriteLine($"{name} 員工招聘培訓管理");
}
class FinanceDepartment : Company // 葉節點,類似 HR
{
// ...
public override void LineOfDuty()
=> Console.WriteLine($"{name} 公司財務收支管理");
}客戶端:
ConcreteCompany root = new ConcreteCompany("北京總公司");
root.Add(new HRDepartment("總公司人力資源部"));
root.Add(new FinanceDepartment("總公司財務部"));
ConcreteCompany comp = new ConcreteCompany("上海華東分公司");
comp.Add(new HRDepartment("華東分公司人力資源部"));
comp.Add(new FinanceDepartment("華東分公司財務部"));
root.Add(comp);
ConcreteCompany comp1 = new ConcreteCompany("南京辦事處");
comp1.Add(new HRDepartment("南京辦事處人力資源部"));
comp1.Add(new FinanceDepartment("南京辦事處財務部"));
comp.Add(comp1);
root.Display(1);
root.LineOfDuty();組合模式的好處#
- 定義了基本物件(人力資源部、財務部)與組合物件(分公司、辦事處)的類別層次結構
- 基本物件可以被組合成更複雜的組合物件,而組合物件又可以被進一步組合,不斷遞歸下去
- 客戶端任何用到基本物件的地方都可以使用組合物件
- 用戶不必為了「處理葉節點」與「處理組合節點」寫選擇判斷
組合模式讓客戶可以一致地使用組合結構和單個物件。
公司開多少個分公司、辦事處——甚至理論上開到地級市、縣、鎮、鄉、村、戶——都不再是問題。