聊聊C#中的composite模式

2022-06-19 12:01:08

寫在前面

Composite組合模式屬於設計模式中比較熱門的一個,相信大家對它一定不像對存取者模式那麼陌生,畢竟誰又沒有遇到過樹形結構呢。不過所謂溫故而知新,我們還是從一個例子出發,起底一下這個模式吧。
 

一個簡單例子

設想我們要建立一個公司的人事架構,在一個公司裡,我們可以簡單地分為兩種員工,一種是經理(包括老闆),另一種是基層員工,經理可以有下屬,而普通員工不行,我們寫出這樣的程式碼。
 

基層員工類

這種員工是最基層的員工,沒有下屬

class BasicLevelEmployee //基層員工
{
    public string ID { get; set; }
    public void ShowStatus(int indent)
    {
        string str = ID;
        str = str.PadLeft(ID.Length + indent, '-');
        Console.WriteLine(str);
    }
}

 

經理類

經理可以有下屬,下屬可能是基層員工,也可能是其他經理(考慮老闆這種情況,無疑其他經理也是老闆的下屬),因為比基層員工多了下屬,所以也多了一些方法維護下屬屬性

class Manager //經理
{
    public string ID { get; set; }
    public void ShowStatus(int indent) 
    {
        string str = ID;            
        str = str.PadLeft(ID.Length + indent, '-');
        Console.WriteLine(str);
        indent += 4;
        Subordinate.ForEach(s => s.ShowStatus(indent));
        SubordinateManagers.ForEach(m => m.ShowStatus(indent));
    }
    public List<BasicLevelEmployee> Subordinate = new List<BasicLevelEmployee>();
    public List<Manager> SubordinateManagers = new List<Manager>();
    //下面是經理所屬的方法
    public void AddSubordinate(BasicLevelEmployee e) { Subordinate.Add(e); }
    public void AddSubordinate(Manager e) { SubordinateManagers.Add(e); }
    public void RemoveSubordinate(BasicLevelEmployee e) { Subordinate.Remove(e); }
    public void RemoveSubordinate(Manager e) { SubordinateManagers.Remove(e); }      
}

 

公司架構類

公司架構類非常簡單,只需要掌握最大的BOSS,整個公司人事架構都可以順藤摸瓜的展示出來

class CompanyHierachy
{
    public Manager BOSS { get; set; }
    public void ShowStatus()
    {
        BOSS.ShowStatus(0);
    }
}

使用者端程式碼

假設這個公司的結構很單純,除了老闆就是開發部門和財務部門,各個部門分設經理是,所以我們寫出程式碼如下

class Program
{
    static void Main(string[] args)
    {
        //老闆
        Manager boss = new Manager() { ID = "BOSS" };
        //開發部門經理
        Manager devManager = new Manager() { ID = "Dev Manager" };
        //財務部門經理
        Manager financeManager = new Manager() { ID = "Finance Manager" };
        //開發組長
        Manager devLead = new Manager() { ID = "Dev Lead" };
        //測試組長
        Manager qcLead = new Manager() { ID = "QC Lead" };

        boss.AddSubordinate(devManager);
        boss.AddSubordinate(financeManager);
        financeManager.AddSubordinate(new BasicLevelEmployee() { ID = "Purchase" });
        devManager.AddSubordinate(devLead);
        devManager.AddSubordinate(qcLead);
        devLead.AddSubordinate(new BasicLevelEmployee() { ID = "Developer1" });
        devLead.AddSubordinate(new BasicLevelEmployee() { ID = "Developer2" });
        qcLead.AddSubordinate(new BasicLevelEmployee() { ID = "QuanityControl1" });
        qcLead.AddSubordinate(new BasicLevelEmployee() { ID = "QuanityControl2" });
        CompanyHierachy company = new CompanyHierachy() { CEO = boss };
        company.ShowStatus();
    }
}

程式碼非常簡單,不需要更多解釋了,執行後得到結果

一切正常,程式碼是工作的,公司架構建立成功了。

再想一下

但是想想,這樣的程式碼真的好嗎?感覺起碼有兩個地方我們可以改進。

  1. 基層員工和經理其實有太多的共性(屬性和方法),可以利用抽象思維,讓他們繼承自同一種東西嗎?
  2. 在經理類中我們維護了多個下屬列表,如果以後再加一個實習生,是不是我們又得建立更多的列表?如果我們使用了繼承,這個問題還會存在嗎?

基於此,利用抽象思維讓經理和員工繼承自同一個類(僱員)勢在必行。在抽象之後,經理類會繼承自僱員並且也內含僱員列表,可能第一次見到這種包含自身父類別列表的設計方式會讓人感覺不習慣,但不用擔心,這其實是一種比較常見的設計方式。這種既有繼承也有合成的結構,就是組合模式的精髓。
 

使用組合模式進行重構

組合模式屬於結構型設計模式,它利用型別層級和聚合層級構造更大的複合結構

說的更加直白一點,當物件的區域性結構和物件自身相同的情況下,我們可以使用繼承加上聚合的方式來組合程式碼,比如剛剛提到的例子中,

觀察一下,對於Boss來說,它的區域性結構,即DevManager和FinanceManager與它自己的結構有何區別?都是樹結構,無非就是根節點不一樣而已,所以於情於理這一塊可以用繼承加聚合來重構
那麼心細的朋友肯定發現了,有些操作是經理類獨有的,這些操作我們是應該抽象到和基層員工共同的父類別僱員類嗎?對於這個問題,一般有兩種解決方案

透明型


在此設計中,子類方法的並集被提煉到了共有父類別,哪怕這些方法對於某些子類根本不需要,這樣的好處是使用者端在使用的時候根本不需要知道物件糾結是哪個子類,對使用者端透明,所以得名。當前設計多采用這種。

安全型


安全型設計非常保守,只會提煉子類交集的方法到父類別,這樣的好處是絕對安全,使用者端絕對不可能在BasicLevelEmployee物件上面呼叫AddSubordinate或者RemoveSubordinate。但有時候會面臨向下轉型的情況。
 

重構後的程式碼(透明型)

抽象出共同父類別僱員類,使用透明型,所有的子類方法都提煉到這個類

abstract class Employee
{
    public string ID { get; set; }
    public abstract void ShowStatus(int indent);
    //因為是透明型,所以基層員工用不上的方法也會被抽象到父類別
    public abstract void AddSubordinate(Employee e);
    public abstract void RemoveSubordinate(Employee e);
}

對於基層員工,如果使用者端無意間呼叫了不該使用的方法,這基本是一個明確的、表明使用者端程式碼出現了邏輯問題的訊號,這種情況直接丟擲異常,能更快地暴露出問題

class BasicLevelEmployee : Employee
{
    public override void ShowStatus(int indent)
    {
        string str = ID;
        str = str.PadLeft(ID.Length + indent, '-');
        Console.WriteLine(str);
    }

    public override void AddSubordinate(Employee e)
    {
        throw new NotImplementedException();
    }
    public override void RemoveSubordinate(Employee e)
    {
        throw new NotImplementedException();
    }
}

在經理類中,得益於共有父類別Employee,我們可以用一個列表裝下所有的下屬,不論下屬是基層員工,還是經理,抑或是未來可能新增的實習生。畢竟他們都是僱員嘛

class Manager : Employee
{
    public override void ShowStatus(int indent)
    {
        string str = ID;
        str = str.PadLeft(ID.Length + indent, '-');
        Console.WriteLine(str);
        indent += 4;
        Subordinate.ForEach(s => s.ShowStatus(indent));
    }
    public List<Employee> Subordinate = new List<Employee>();
    //下面是經理所屬的方法
    public override void AddSubordinate(Employee e) { Subordinate.Add(e); }
    public override void RemoveSubordinate(Employee e) { Subordinate.Remove(e); }
}

公司架構類和使用者端程式碼呼叫保持不變,執行結果一致,重構成功。
 
可以看到,在使用了組合模式之後,現在的程式碼不但消除了冗餘(不用再去維護多個下屬列表),也更具有抵禦未來變化的能力,這樣的結構比起原來,當然是更加合理的。這就是結構型設計模式的用武之地,讓物件的結構更加的合理,更加的易於擴充套件。
這就是關於Composite組合模式的介紹,鑑於筆者能力有限,如果大家對於這篇文章中所講有其他看法,歡迎留言討論。