引子:分公司不就是一個部門嗎?#

小菜公司接到客戶的辦公管理系統,總部有人力資源部、財務部、運營部等。但客戶想把同一套系統推廣到全國各地的分公司、辦事處:

  • 北京總公司
    • 人力資源部、財務部
    • 上海華東分公司
      • 人力資源部、財務部
      • 南京辦事處、杭州辦事處
        • 人力資源部、財務部

這是「部分與整體」的關係——分公司、辦事處與總公司是樹狀結構,不是平行的。

類似情境很多:

  • 賣電腦:可以單獨配件,也可以組裝整機
  • 複製文件:單檔複製,也可以整資料夾複製
  • 文字編輯:單字加粗,也可整段加粗

組合模式#

組合模式(Composite Pattern):將物件組合成樹形結構以表示「部分-整體」的層次結構。組合模式使得用戶對單個物件組合物件的使用具有一致性。[DP]

結構#

  • Component:為組合中的物件聲明介面,並在適當情況下實現所有類別共有介面的預設行為;聲明用於訪問和管理子部件的介面
  • Leaf(葉節點):在組合中表示葉節點物件,沒有子節點
  • Composite(組合):定義有枝節點行為,用來存儲子部件,實現與子部件有關的操作(如 AddRemove
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 : children
abstract 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. 安全方式#

透明方式#

AddRemove 都聲明在 Component 中——所有子類都具備這些介面。

優點:葉節點與枝節點對外完全一致的介面,客戶端無需判斷型別。

缺點:Leaf 本身不需要 AddRemove,實作它們沒有意義(強制呼叫會出錯)。

安全方式#

AddRemove 移到 Composite 才聲明,Leaf 不必實作。

優點:葉節點不會誤被呼叫 Add/Remove

缺點:兩者介面不再相同,客戶端要判斷類型——不夠透明。

兩者各有取捨,視情況決定。

何時使用?#

當需求中是體現部分與整體的層次結構,且希望用戶忽略組合物件與單個物件的不同、統一地使用結構中的所有物件時,就應該考慮組合模式。

實務範例:

  • ASP.NET 的 TreeView 控件
  • 自訂控件:把基本控件組合成複合控件——所有 Web 控件的基類 System.Web.UI.Control 都有 AddRemove 方法

範例:公司管理系統#

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();

組合模式的好處#

  • 定義了基本物件(人力資源部、財務部)與組合物件(分公司、辦事處)的類別層次結構
  • 基本物件可以被組合成更複雜的組合物件,而組合物件又可以被進一步組合,不斷遞歸下去
  • 客戶端任何用到基本物件的地方都可以使用組合物件
  • 用戶不必為了「處理葉節點」與「處理組合節點」寫選擇判斷

組合模式讓客戶可以一致地使用組合結構和單個物件。

公司開多少個分公司、辦事處——甚至理論上開到地級市、縣、鎮、鄉、村、戶——都不再是問題。