從反覆器模式到合成模式(Composite Pattern)

從反覆器模式到合成模式(Composite Pattern)

這將是一個很長很長的故事,接續先前談了反覆器模式的基礎

故事是兩個公司合併成一個集團時,員工系統的整合

前情回顧:

Employee物件(有改良過)

   1: public class Employee
   2: {
   3:     public Employee(string pName, string pDept,string pTitle,string pEmail)
   4:     {
   5:         this.Name = pName;
   6:         this.Dept = pDept;
   7:         this.Title = pTitle;
   8:         this.Email = pEmail;
   9:     }
  10:  
  11:     public string Name{get;set;}
  12:     public string Dept { get; set; }
  13:     public string Title { get; set; }
  14:     public string Email { get; set; }
  15: }

我們創建了一個I_Iterator的介面,供各個子公司的清冊實作共同的行為(hasNext、next)

然而,其實上次也提到C#中已經提供了反覆器供我們使用

為什麼我們可以用Foreach來走訪整個集合呢?(像是Dictionary跟Array..etc)

這些集合物件都實踐了名稱為IEnumerator的介面,除此之外

為了讓客戶能直接使用所有具備反覆器的物件,所以創造了一個聚合的介面。

但要支援Foreach只有IEnumerator還不夠,還必須實踐了IEnumerable的介面(參考)

對比到上次我們的案例中,我們可以知道原來,IEnumerator就是所謂的I_Iterator

IEnumerable就是所謂的GroupEmpList(見反覆器模式的文章)

假如現在集團又購併了另一間公司呢?

剛好這間公司的員工資料是透過Dictionary在儲存的!

用範例程式碼來說明好了

我們來實踐這個介面看看,來改寫我們的員工清冊的系統吧

   1: public class C_Company_EmpEnum : IEnumerator
   2:  {
   3:   private Dictionary<string,Employee> employees;
   4:   int position = -1;
   5:  
   6:   /// <summary>
   7:   /// 建購式
   8:   /// </summary>
   9:   /// <param name="employees"></param>
  10:   public C_Company_EmpEnum(Dictionary<string, Employee> employees)
  11:   {
  12:     this.employees= employees;
  13:   }
  14:  
  15:  
  16:   #region IEnumerator 成員
  17:  
  18:   //取得現有成員
  19:   public object Current
  20:   {
  21:       get
  22:       {
  23:         try
  24:         {
  25:             return employees.ElementAt(position);
  26:         }
  27:         catch (IndexOutOfRangeException)
  28:         {
  29:             throw new InvalidOperationException();
  30:         }
  31:       }
  32:   }
  33:  
  34:   public bool MoveNext()
  35:   {
  36:       position++;
  37:       return (position < employees.Count);
  38:   }
  39:  
  40:   public void Reset()
  41:   {
  42:       position = -1;
  43:   }
  44:   #endregion
  45:  }

IEnumerator規範我們必須實作三個方法:Current、MoveNext、以及Reset

(就像前一篇所謂的HasNext以及MoveNext方法,只是換一個思考的角度而己)

那透過這個就可以讓反覆器被Foreach存取了嗎?其實尚未完成:

接著來實踐一個共同的介面來取得相關的反覆器吧

實踐IEnumerable

依循前例,這間新購併的公司員工清冊的程式碼:

因為合併到集團,所以要依循集團的人力資源系統

記得我們先前是透過反覆器模式,將不同的集合型態都可以整合起來

   1: //實踐集團公司中員工清冊的介面
   2: public class C_Company_EmpList: IEnumerable
   3: {
   4:     Dictionary<string,Employee>
   5:         gEmplist = new Dictionary<string, Employee>();
   6:     //初始化的時候,會取得資料庫的員工清單
   7:     public C_Company_EmpList()
   8:     {
   9:         //....略,我們可以將資料存取程式碼建立於此,例如讀取資料庫
  10:     }
  11:  
  12:     public void addEmployee(string pEmpID, Employee pEmployee)
  13:     {
  14:         //新增一個員工
  15:         gEmplist.Add(pEmpID, pEmployee);
  16:     }
  17:  
  18:     //這個方法我們要避開,正因為這個直接ref他的HashTable型別
  19:     //所以無法讓客戶輕易的使用它,乾脆刪掉他…
  20:     //public Hashtable getEmplist()
  21:     //{
  22:     //    //取得員工清冊
  23:     //    return gEmplist;
  24:     //}
  25:  
  26:     //實踐集團取得員工清冊的方法,我們僅實作取得值的部分的反覆器
  27:     //因為為了跟其他的反覆器一致
  28:     #region IEnumerable 成員
  29:     public IEnumerator GetEnumerator()
  30:     {
  31:         return new C_Company_EmpEnum(gEmplist);
  32:     }
  33:     #endregion
  34: }

讓集團員工清冊系統正式認識A、B、C三家子公司的員工清冊(正式整合)

   1: /// <summary>
   2:     /// 集團的員工清冊系統
   3:     /// </summary>
   4:     public class GroupEmpListSystem
   5:     {
   6:         IEnumerable A_Company_EmpList;
   7:         IEnumerable B_Company_EmpList;
   8:         IEnumerable C_Company_EmpList;
   9:  
  10:         //將子公司的所有清冊整合
  11:         public GroupEmpListSystem(IEnumerable pA_Company_EmpList, IEnumerable pB_Company_EmpList, IEnumerable pC_Company_EmpList)
  12:         {
  13:             this.A_Company_EmpList = pA_Company_EmpList;
  14:             this.B_Company_EmpList = pB_Company_EmpList;
  15:             this.C_Company_EmpList = pC_Company_EmpList;
  16:         }
  17:  
  18:         private void printList()
  19:         {
  20:             IEnumerator tA_Company_EmpEnum = new A_Company_EmpList().GetEnumerator();
  21:             IEnumerator tB_Company_EmpEnum = new B_Company_EmpList().GetEnumerator();
  22:             IEnumerator tC_Company_EmpEnum = new C_Company_EmpList().GetEnumerator();
  23:  
  24:             printList(tA_Company_EmpEnum);
  25:             printList(tB_Company_EmpEnum);
  26:             printList(tC_Company_EmpEnum);
  27:         }
  28:  
  29:         //統一印出清冊的API
  30:         private void printList(IEnumerator pIE)
  31:         {
  32:             while (pIE.MoveNext())
  33:             {
  34:                 //印出名單
  35:                 Console.WriteLine(((Employee)pIE.Current).Name);
  36:             }
  37:         }
  38:     }

上述程式碼,透過第30行的反覆器共同的行為模式,已經可以列出所有子公司的清單了(註:A、B、C公司都已經換成了.NET中的IEnumerable的反覆器聚合)

然而,我們必須承認,集團員工清冊系統還有一些問題

首先,他呼叫了三次GetEnumerator(),建立了三個反覆器

每當每次我們一購併了新的公司,或是整合了新的公司

我們就要打開這個系統,加入集團員工清冊系統的程式碼

在第20行那邊加上一個新的公司

這違反了我們的開放關閉守則

目前這個系統要呼叫printList三次,

是否有辦法將整個清冊合併變成只呼叫一次?

或者只傳給這個系統一個反覆器就好了

而利用這個反覆器就可以在所有的公司清冊中遊走?

我們換個角度,現在集團必須支援集團內所有組織(部門、中心、團隊、子公司)的清冊

針對上述的需求,我們定義為:

1.支援多個組織的清冊(清冊的集合

除此之外,集團員工清冊系統還要支援清冊中的清冊

原因是因為我們未來也想把這樣的清冊落實到所有的子公司

甚至是部門還有所有的專案團隊,讓他們只要透過反覆器

就可以使用清冊的結構。

總之

2.我們還要支援”清冊中的清冊”,這是另一個需求的重點

而我們不要把子公司的所有部門清冊都算在集團的公司清冊中

因為這樣會破壞了階層關係。

因為我們需要表現集團層級、公司層級、部門層級的員工清冊

自然地採用樹狀結構以便符合這樣的需求。

image

看來我們又要改程式了…

定義合成模式::(摘錄於HeadFirst Design Pattern一書)

允許你將物件合成樹狀結構,呈現「部份/整體」的階層關係

合成能讓客戶程式碼以一致的方式處理個別物件,以及合成的物件。

合成模式的類別圖:

image

由圖就可以明白,合成包含了元件,而元件有兩種:合成與葉元素。

這是眾所熟悉的遞迴式結構對吧,過去寫過C的經典範例都寫過遞迴程式(Tree-Leaf-Node)

合成元件包含了許多子代,合成的分支會逐漸往下延伸,直到樹葉為止

透過這樣的方式來組織資料,就會得到一組由上而下的樹狀結構。

所以我們要利用合成來設計我們的集團的清冊結構

image

我們在集團元素的抽象類別中定義了一些方法。這是為了讓我們能夠用一套做法

就可以同時處理Employee(員工)與GroupComponentList(部門、團隊、中心、子公司等集合員工的聚集)

接著我們看看如何修改程式,讓清冊能夠支援合成模式吧:

   1: //我們對集團元素定義一個抽象元件,它對每個方法都提供一個預設的實踐方式
   2:     //有一些方式跟前面所說的葉節點(Employee)有關,有一些是跟集團元素集合(GroupComponentList)有關
   3:     //但不是所有方法都適用所有的節點,因此面對這種狀況最好丟出Exception
   4:     public abstract class GroupComponent
   5:     {
   6:         public void add(GroupComponent pGroupComponent)
   7:         {
   8:             throw new Exception("不支援此方法");
   9:         }
  10:  
  11:         public void remove(GroupComponent pGroupComponent)
  12:         {
  13:             throw new Exception("不支援此方法");
  14:         }
  15:  
  16:         public void print()
  17:         {
  18:             throw new Exception("不支援此方法");
  19:         }
  20:  
  21:         public string getComponentName(GroupComponent pGroupComponent)
  22:         {
  23:             throw new Exception("不支援此方法");
  24:         }
  25:  
  26:         public GroupComponent getChild(int i)
  27:         {
  28:             throw new Exception("不支援此方法");
  29:         }
  30:     }

上述我們定義了集團元素的抽象類別

目的就是為了讓葉節點(Employee)與中間節點(GroupComponentList;合成)共同繼承

然而目前要為這些方法提供預設的實踐方式。如果葉節點或是合成不想實踐的話

就不需要覆寫這些方法。

接著來實踐葉節點看看(Employee)

   1: public class Employee : GroupComponent
   2:     {
   3:         string name; //姓名
   4:         string title;//職稱
   5:         string ext;  //分機
   6:         string email;//電郵
   7:  
   8:         public Employee(string name, string title,string ext,string email)
   9:         {
  10:             this.name = name;
  11:             this.title = title;
  12:             this.ext = ext;
  13:             this.email = email;
  14:         }
  15:         //視為一個元素,取得元素名稱
  16:         public override string getComponentName()
  17:         {
  18:             return name;
  19:         }
  20:  
  21:         // 印出完整的員工資訊(葉節點)
  22:         public override void print()
  23:         {
  24:             Console.WriteLine(string.Format("Name:{0},Title:{1},Ext:{2},Email:{3}", name, title, ext, email));
  25:         }
  26:     }

這個是合成類別圖中的葉節點,也是我們改編過的Employee節點(公司組織的葉節點)
請注意,本類別並未推翻Add與Remove的方法,因為我們的葉節點並不支援這兩個操作。因為員工已是最小單位了,而也只有組織元素,才可以去異動成員。

如果有人嘗試著呼叫Employee的Add方法,將得到不支援此方法的Exception

最後我們要來實踐合成節點了(GroupComponentList)

   1: //定義合成類別,這代表著尚未到組織的最小單位(員工),而這也有可能是一個中心(包含多個部門)或是一個部門(包含多個員工)...etc
   2:     public class GroupComponentList : GroupComponent
   3:     {
   4:         List<GroupComponent> GroupComponents = new List<GroupComponent>();
   5:         string name;
   6:         string description;
   7:  
   8:         public GroupComponentList(string name , string description)
   9:         {
  10:             this.name = name;
  11:             this.description = description;
  12:         }
  13:  
  14:         public override void add(GroupComponent pGroupComponent)
  15:         {
  16:             this.GroupComponents.Add(pGroupComponent);
  17:         }
  18:  
  19:         public override void remove(GroupComponent pGroupComponent)
  20:         {
  21:             this.GroupComponents.Remove(pGroupComponent);
  22:         }
  23:  
  24:         public override GroupComponent getChild(int i)
  25:         {
  26:             return this.GroupComponents.ElementAt(i);
  27:         }
  28:  
  29:         public override string getComponentName()
  30:         {
  31:             return name;
  32:         }
  33:  
  34:         public override void print()
  35:         {
  36:             Console.WriteLine(string.Format("Name:{0},Description:{1}", name, description));
  37:         }
  38:     }

等一下,這邊的print的方法怪怪的

如果這樣對合成節點呼叫的話,不是就只會出現簡單的名稱與描述嗎?

因此我們要修正print的方法

   1: public override void print()
   2: {
   3:     Console.WriteLine(string.Format("Name:{0},Description:{1}", name, description));
   4:  
   5:     //熟悉的程式碼,這次就是從一個反覆器的集合取出來而己,而當中又可能是另一個反覆器的集合
   6:     IEnumerator tIEnumerator = this.GroupComponents.GetEnumerator();
   7:     while (tIEnumerator.MoveNext())
   8:     {
   9:         GroupComponent tGC = ((GroupComponent)tIEnumerator.Current);
  10:         //請注意,在反覆的期間,如果遇到另一個GroupComponentList的合成物件
  11:         //就會呼叫他的print()方法,來造成另一個反覆
  12:         tGC.print();
  13:     }
  14: }

這樣我們的集團清冊系統將變的非常的簡單,未來只要將最上層的集團元素(整個集團)

交給清冊系統,並呼叫最上層元素的Print()方法,就可以列印出所有組織內,由上到下的員工了…,同時我們具備了管理清冊的方法(Add與Remove)。

最後總結一下合成模式的設計意涵:(摘錄於HeadFirst Design Pattern一書)

合成模式以單一責任的設計守則來換取透明性(Transparency)

所謂的透明性,就像我們上面定義集團元素(GroupComponent)的抽象類別時

讓該介面同時包含管理子節點的操作以及葉節點的操作。

如此一來,客戶就可以將合成節點和葉節點一視同仁。

也就是說,客戶不需要知道目前元素到底是員工,還是組織(部門或公司)。

只是這樣我們也失去了一些安全性,客戶有機會做一些不適當或沒有意義的設計。這是設計上的抉擇,當然我們也可以將責任切開,放在不同的介面

只是這樣也失去了透明性。

合成與反覆器的結合:

首先我們先在 GroupComponent的抽象類別中,直接實踐IEnumerable 的介面

(因此必須強制加入以下方法)

   1: #region IEnumerable 成員
   2: public IEnumerator GetEnumerator()
   3: {
   4:     throw new Exception("不支援此方法");
   5: }
   6: #endregion

接著讓Employee與GroupComponentList來實踐這個方法。

(因為繼承,直接覆寫)

Employee:

   1: public override IEnumerator GetEnumerator()
   2: {
   3:     return new NULLIEnumerator(); //空反覆器,用於定義沒有項目可以遊走的狀態
   4: }

GroupComponentList:(這個元素使用了一個新的合成反覆器,容以下說明)

   1: public override IEnumerator getEnumerator()
   2: {
   3:     return new CompositeIEnumerator(this.GroupComponents.GetEnumerator());
   4: }

空反覆器:(至於為什麼要定義,是因為客戶不再需要擔心是否傳回值為null

這個反覆器永遠都會傳回False值。

   1: public class NULLIEnumerator : IEnumerator
   2: {
   3:  
   4:     #region IEnumerator 成員
   5:  
   6:     public object Current
   7:     {
   8:         get { return null; }
   9:     }
  10:  
  11:     public bool MoveNext()
  12:     {
  13:         return false;
  14:     }
  15:  
  16:     public void Reset()
  17:     {
  18:         throw new NotImplementedException();
  19:     }
  20:  
  21:     #endregion
  22: }

合成反覆器:

   1: /// <summary>
   2: /// 合成反覆器,負責反覆地在組織內的項目遊走,實踐了反覆器IEnumerator介面
   3: /// </summary>
   4: public class CompositeIEnumerator : IEnumerator
   5: {
   6:     Stack stack = new Stack();
   7:  
   8:     /// <summary>
   9:     /// 將我們所欲造訪的最上層合成節點的反覆器直接轉接來當作建構式的參數,丟進堆疊中
  10:     /// </summary>
  11:     public CompositeIEnumerator(IEnumerator pIE)
  12:     {
  13:         stack.Push(pIE);
  14:     }
  15:  
  16:     #region IEnumerator 成員
  17:  
  18:     //取得現有成員
  19:     public object Current
  20:     {
  21:         get
  22:         {
  23:             try
  24:             {
  25:                 //當客戶想要取得下一個元素的時候,我們先呼叫MoveNext來確認是否還有下一個
  26:                 if (MoveNext())
  27:                 {
  28:                     IEnumerator tIEnumerator = (IEnumerator)stack.Peek();
  29:                     GroupComponent component = (GroupComponent)tIEnumerator.Current;
  30:                     //若還有下一個,則從堆疊中取出來,並取得目前的元素。
  31:                     if (component.GetType() == typeof(GroupComponentList))
  32:                     {
  33:                         //如果取出來的元素是一個合成元素,稍後需要更進一步深入這個節點
  34:                         //所以我們將它丟進堆疊中,不論如何最終都要將該元素當作傳回值
  35:                         stack.Push(component.GetEnumerator());
  36:                     }
  37:                     return component;
  38:                 }
  39:                 else
  40:                 {
  41:                     return null;
  42:                 }
  43:             }
  44:             catch (IndexOutOfRangeException)
  45:             {
  46:                 throw new InvalidOperationException();
  47:             }
  48:         }
  49:     }
  50:  
  51:     public bool MoveNext()
  52:     {
  53:         if (stack.Count == 0)
  54:         {
  55:             //想要知道是否還有下一個元素,則檢查堆疊是否被清空了。
  56:             return false;
  57:         }
  58:         else
  59:         {
  60:             //不然的話,我們就從堆疊的最上層取出反覆器,來確認是否還有下一個元素。
  61:             //如果沒有的話,我們就將它取出堆疊,然後遞回呼叫MoveNext()
  62:             IEnumerator tIEnumerator = (IEnumerator)stack.Peek();
  63:             if (!tIEnumerator.MoveNext())
  64:             {
  65:                 stack.Pop();
  66:                 return MoveNext();
  67:             }
  68:             else
  69:             {
  70:                 return true;
  71:             }
  72:         }
  73:     }
  74:  
  75:     public void Remove()
  76:     {
  77:         new Exception("不支援移除");
  78:     }
  79:     #endregion
  80: }

上述的程式與我們原先的程式有何不同呢?

我們原先實作的是「內部」的反覆器

GroupComponent、Emplyee、GroupComponentList是會利用

print的方法來利用反覆器來走訪元素內的項目。

而合成反覆器是一種「外部」的反覆器,所以有許多需要追蹤的事情。

外部反覆器必須進入它的位置,以便問是要呼叫MoveNext還是Current

在這個例子中,我們的程式碼也在記錄合成節點內部的位置,用的是遞迴。

也用堆疊記錄自己的位置。

最後分享Head First Design Pattern認為合成模式最大的特色吧:(摘錄於HeadFirst Design Pattern一書)

讓客戶不再需要擔心面對的是合成物件或是樹葉物件,所以就不需要寫一堆if敘述,針對不同型態的物件呼叫不同的方法。通常,只需要呼叫一個方法就可以了,根本不需要管他是什麼型態的物件。

參考資料:

HeadFirst Desigh Pattern

MSDN (參考)