分類: DesignPattern

支援TestContainer資料庫整合自動化測試的Clean Architecture專案模版-2024年版總結

支援TestContainer資料庫整合自動化測試的Clean Architecture專案模版-2024年版總結

在前幾份工作透過課程學過單元測試與重構,工作領域就是較多的傳統三層式架構(ex: Presentation-Service-Repository),直到前一份接觸到了實務的Clean Architecture方案開始(約莫三年),發現了C#開發的新天地,終於有一種先前課程單元測試與重構、極速開發學到的技巧有機會應用到工作上的感覺。

因此最近在多個產品領域逐步試驗抽換,有些產品是從0-1,我覺得相對容易,只要開發Pattern形成,其實風格跟文化就會自動形成。

大概的要素如下:

  1. Clean Architecture的專案模版(EFCore .NET+ MediatR-CQRS)
  2. 自動化 IntegrationTest + UnitTest

Clean Architecture的專案模板(EFCore .NET+ MediatR-CQRS)

傳統 N 層架構與 Clean Architecture 的差異

最大的不同在於 依賴注入(DI) 的引入後,Infrastructure 層參考 Application 層,而 API 層 則參考 Application 層MediatR 物件。MediatR 提供了一種 標準化的行為,讓使用層(Client 或 API)可以透過抽象方法進行呼叫(類似 Invoke Action)。透過 IRequest + IRequestHandler,請求會標準化地傳遞至 Application 層 執行。這樣的設計提高了 可測試性,因為大部分的 業務邏輯內聚在 Application 層,不像傳統架構可能將部分邏輯散落在 Web API 等層級中。當然,開發者仍然可以 不遵循原則,把邏輯寫得分散,這就需要有意識地去避免這種情況。在這樣的架構下,Web API 的 Controller 只需專注於與 IHttpContext 交互,或處理 Request-Level 事務,例如: Mapping Command / Query 物件

到了 Application 層,應該只剩下 業務邏輯,不應與 HttpContext 這類原生或底層物件耦合。這樣能大幅提高 Application 層的可測試性。反之Infrastructure 層 應該完全封裝對外部系統(如 Redis、資料庫(DB)、其他第三方套件)的實作,外部專案只能透過 Interface 操作,確保解耦與可維護性。

Clean Architecture 中,DbContext 屬於 Infrastructure 層,而 Entities 則歸屬 Domain 層。實際上,Entity 本質上就是資料庫表的物件對應。如果 Schema 設計符合 Use Case,資料操作起來就會 更直覺,就像在處理業務邏輯,而不只是冷冰冰的 SQL 操作(如 Insert、Update、Delete)。

使用了EntityFramework後,雖然我們可以透過 DI 綁定 IDBContext,讓任意使用端操作資料庫。這樣的做法 雖然方便,但不建議長期使用。或許在 開發初期,這種方式有助於 自動化測試,因為 TDD 產生的測試可以直接驗證資料庫行為。然而,當系統進入 重構階段,應將 IDBContext 介面Application 層 提取到 Service 層,這樣的調整 將會非常輕鬆,同時也能讓架構更符合 Clean Architecture 原則

自動化 IntegrationTest + UnitTest

單元測試的 3A 原則(Arrange, Act, Assert),以及 Mock、Fake、Stub 等技巧固然重要,但這裡想強調的是一個 觀念上的轉變。我認為 單元測試不僅僅是函式層級的測試,而是 只要測試對象可以完全控制,就能算是單元測試。如果一個函式綁定了資料庫操作,是否還能進行單元測試?我認為可以。只要能在測試前準備好資料庫,並在測試結束時清除資料,那麼就能夠 更真實地模擬函式的行為

這樣的做法避免了繁瑣的 Mock 設定,因此 TestContainer 應運而生。過去,準備一個測試用資料庫相對麻煩,不僅需要在本機安裝,還要在 CI/CD 環境中對應不同的資料庫實例。但隨著 Docker 的出現,以及 微軟支援 Docker 版 SQL Server,這個問題得到了有效解決。因此,因為「資料庫」難以”測試”已不應再成為藉口

曾有人問我,許多業務邏輯因為過度依賴資料庫,而導致難以進行良好的單元測試。常見的解法是 將資料庫操作封裝進 IRepository,但接下來的問題是:我們是否應該對 IRepository 進行 Mock?我的結論是:看需求與團隊接受程度。如果我們可以 自動建置資料庫,且 IRepository 主要對接資料庫,那麼就應該讓測試驗證實際的資料庫操作。如果不測試,我認為測試是不完整的,這樣的測試結果也不太令人安心。至於 IProxy 介接外部 API 的部分,我認為這屬於 異質系統,通常交由外部團隊維護,因此建議 Mock 掉

TestContainer 與 Integration Test

在了解 TestContainer 的概念後,我們的目標是讓 每次測試(包含 CI Pipeline)都能進行 Integration Test。因此,我建議採取以下做法:

1. 透過 Docker 自動建立資料庫

2. 自動建置 Schema(可參考 Repository 中 IntegrationTest 的 BeforeRun)

3. 自動升級資料庫版本,確保 Application 依賴的資料庫狀態正確

如此一來,撰寫每個 Test Case 時,數據層都能確保已被測試。

最後,我建議搭配 Database.Reset 套件(目前支援 SQL ServerPostgreSQL),以便快速重置資料庫,確保每次測試環境的一致性。這樣一來,我們就能實踐 「整合單元測試」(Integration Unit Test)。

維護成本與 Schema 同步

維護成本主要來自於 Schema 的同步,這部分可由 DBA 維護,或 團隊自行定義同步方式。這裡沒有標準做法,重點是讓流程 舒適、易於維護,才能長期執行下去。

這些做法在我們團隊已經試驗超過半年,整體來說 可行,團隊成員也已經習慣撰寫測試,並且包含 數據庫驗證測試。目前,我正協助 其他產品團隊 導入 自動化數據庫測試流,調整適合他們的方案。

當然,這個過程難以避免 一定程度的重構,而且不同方案各有風險。由於大多數系統缺乏良好的測試保護,導致改造充滿挑戰。然而,透過 測試技巧,可以逐步 抽離 Interface(這是我認為最重要的一步)。

整體來看,主要的挑戰在於 改造策略與推進進度,但這 並非技術或測試不可行的問題。這部分有機會的話,我會在後續進一步分享。

Clean Architecture 的一般化方案

為了讓 微軟 C# Application 能夠 快速理解並導入 Clean Architecture,我對這套架構進行了一定程度的 一般化整理,並已經在 GitHub 上建立範本。這不僅是個紀錄,也能作為 範本,方便有需要的團隊 直接導入或參考

連結如下: https://github.com/pin0513/ECAppForCleanArchitecture

todo: 會持續將 MediatR的案例整合進去哦

[Design Pattern] 簡單工廠與工廠方法模式

[Design Pattern] 簡單工廠與工廠方法模式

近期雖然已經從c#轉向python,但發現可以用到OO的地方,可以建造工廠的情境也非常地多,索性把之前的文章拿來反覆咀嚼,挺有趣

這一篇雖然是2010年寫的,介紹重量級的工廠模式-定義來自於深入淺出設計模式一書,而本例重新構思案例,適圖舉一反三 😛

 

前幾篇提到了三個不同型式的Design Pattern,分別是Strategy(角色扮演)、Observers(Blog Rss)以及Decorator(早餐店)。傳送門

可以發現,在現有的OO法則下,可以達到在類別設計與使用上的彈性與擴充

我們可以在執行期new出一個具有彈性的具像類別以及透過各種型式來動態地變更物件

 

因為透過介面來操作物件,在類別的操作變得彈性了

就像先前角色扮演的例子一樣,可以new一個Character類別,叫作弓箭手的”角色”

Character Mary = new Archer(); //宣告一個角色叫作Mary,給予他具象的弓箭手類別

上述看到,當『new』的時候,就要聯想到『具象』

什麼是具象?

1

上圖中抽象與具象剛好是一種相對的關係,我們透過不同的具象類別(不同職業的角色)來描述所謂的角色

具象類別就是我們要真正”實體化”的類別,當我們使用new的時候,雖然透過Character作為介面讓程式具有彈性,但是我們還是得建立具象類別(Archer)的實體。

所以用的確實是實踐,而不是介面。這是一個好問題,程式綁著具象類別,將導致程式容易損壞且沒有彈性!

 

為什麼??當有一群相關的具象類別的時候,通常會寫出這樣的程式:

  Character Mary;
  int select=1; //選擇不同職業的角色
   
         if (select==1)
         {
             Mary = new Archer();
         }else if(select==2)
         {
             Mary = new Knight();
         }else if(select==3)
         {
             Mary = new Magician();
         }

有一大堆不同的角色類別,但是必須要到執行期才知道該實體化哪一個職業!

當看到這樣的程式碼,一旦有變動或擴充,就必須打開這個源始碼,加以檢視與修改。通常這樣修改過的程式碼,將造成部份系統更難以維護與更新,而且也有犯錯的可能性。

 

然而,在技術上,new這個字眼沒有問題,真正的犯人是『需求的改變』,以及這個的改變對new的影響力!

回顧一下裝飾者模式所說的關閉開放法則。

類別應該開放,以便擴充;應該關閉,禁止修改

如果針對介面寫程式,可以隔離掉以後系統可能發生的一大堆改變。
介面可以透過多型進行新類別的實踐,而不需要動到原本的程式碼。
但是當程式碼使用了大量的具象類別時(例如角色的職業),那麼一互加入新的具象類別,就必須改變程式碼。

也就是說程式並非『對修改關閉』。當想要擴充新的職業,竟然要重新開放應該封閉的程式碼。

那麼解決方法並不是無跡可尋的,回想看看第一個OO守則

找出會改變的地方,然後將這部份抽離出來

剛好可以用來處理我們的老朋友『改變』!

 

首先,找出會改變的東西。

 

先來個情境:

現在假如我們仍然是一個手機的開發商,目前有一個提供手機製造並出貨的服務,現在準備發展手機銷售部門,為了實行需求導向

這間公司由不同的手機款式需求,建立原型,再實行製造流程並出單。

身為這間手機製造商的訂單系統開發者,程式碼可能會這麼寫:

  CellPhone orderCellPhone(){
      CellPhone cellphone= new CellPhone(); //建立手機原型(確定硬體設計圖與軟體規格)
    
      cellphone.design();  //動工設計
      cellphone.modeling(); //動工建模
      cellphone.combine(); //動工組合手機
      cellphone.box(); //動工包裝
      return cellphone;  //出貨
  }

註:為了讓系統有彈性,我們會希望CellPhone是一個抽象類別或介面,但是這樣的話,這些類別或介面就無法實體化了…註2,軟體的安裝在組合手機的以後會安裝上去(裡面又呼叫了install()方法)

但是當我們需要更多型式的手機…這時勢必增加一些程式碼,來決定適合的手機具象類別,然後製造這隻手機:

假如有三款型式,分別是Business(商用)、Sport(戶外防水)、Normal(陽春款)

程式碼就會變成:

 CellPhone orderCellPhone(String style){
     CellPhone cellphone;
     if (style.equals("Business")){
         cellphone= new BusinessPhone();  //實踐商用手機
     }else if (style.equals("Sport")){
         cellphone= new SportPhone();     //實踐運動手機
     }else if(style.equals("Normal")){
         cellphone= new NormalPhone();    //實踐陽春手機
     }
  
     cellphone.design(); //設計
     cellphone.modeling(); //建模
     cellphone.combine(); //組裝
     cellphone.box(); //包裝
     return cellphone;
 }

市場競爭日趨激烈,壓力來自於必須快速推出更多型式與型號的手機。而新款的Sport手機要上市,舊款的Sport也要因此從型錄中下市:

  CellPhone orderCellPhone(String style){
  CellPhone cellphone;
  if (style.equals("Business")){
      cellphone= new BusinessPhone();
  //}else if (style.equals("Sport")){  //舊款要下市的手機
  //    cellphone= new SportPhone();
  }else if (style.equals("Music")){   //新款的音樂手機
      cellphone= new MusicPhone();
  }else if (style.equals("Sport2")){  //2代的Sport手機
      cellphone= new Sport2Phone();   
  }else if(style.equals("Normal")){
      cellphone= new NormalPhone();
  }
   
  cellphone.design(); //設計
  cellphone.modeling(); //建模
  cellphone.combine(); //組裝
   cellphone.box(); //包裝
   
  return cellphone;
   
  }

此程式碼並沒有對修改封閉,看到了嗎?如果手機開發商一改變型錄,供應不同的機型,就得修改那些if else。這裡假以時日一遇到改變,就必須一改再改。

而不想改變的地方是設計、建模與組裝的流程,因為手機的製造流程都持續不變,所以這部分的程式碼不會改變,只有手機的需求的款式用途會改變。

所以很明顯,如果實體化某些具像類別(例如音樂手機、運動手機…etc)將使orderPhone( )出問題,而且也無法對它修改關閉,但是目前已經知道哪些會變哪些是不變的,所以是時候來使用封裝了!!

 

首先要先封裝建立物件的程式碼!記得,封裝的目標總是那些最常改變的地方,要把建立物件的程式碼從orderPhone方法中抽離,然後將部分的程式碼搬到另一個物件中,這個新物件只管如何製造手機的原型,如果一旦任何物件想要製造手機原型,找它就對了!

稱這個新物件為『工廠』:PhoneFactory

工廠是負責建立物件的細節,一旦有了PhoneFactory,OrderPhone就變成了PhoneFactory的客人,當需要建立手機的時候,就叫手機工廠做一個。

而以後orderPhone就不需要知道音樂手機還是商用手機,orderPhone只需要從工廠得到一隻手機原型,而這個手機實踐了CellPhone的介面,以呼叫設計、建模、組裝方法,來完成手機。

 

先來建立一個簡單的手機工廠,要先從工廠本身開始,定義一個類別,為所有手機封裝建立物件的程式碼。程式碼就像這樣:

   class PhoneFactory
   {
       public CellPhone createCellPhone(string style){
           CellPhone cellphone = null;
           if (style.equals("Business"))
           {
               cellphone = new BusinessPhone();
               //}else if (style.equals("Sport")){ //舊款要下市的手機
               //    phone = new SportPhone();
           }
           else if (style.equals("Music"))
           {   //新款的音樂手機
               cellphone = new MusicPhone();
           }
           else if (style.equals("Sport2"))
           {  //2代的Sport手機
               cellphone = new Sport2Phone();
           }
           else if (style.equals("Normal"))
           {
               cellphone = new NormalPhone();
           }
    
           return cellphone;
       }
   };

這樣做有什麼好處?似乎只是將問題搬到另一個物件罷了,問題依然存在啊?

提醒一下,OO中封裝還有一個好處就是reuse,雖然目前只看到orderCellPhone()方法是他的客人,然而,可能還有CellPhoneShopMenu類別,會利用這個工廠來取得手機的描述與單價。可能還有宅配的功能需要它,會有不同的方式來處理手機(CellPhoneShop)。所以把建立手機的程式碼包裝進一個類別,當以後實踐改變時,只需要這個類別即可。

 

接著是該來修改客人這一邊的程式碼了

  public class CellPhoneSale
  {
      CellPhoneFactory factory;
   
      public CellPhoneSale(CellPhoneFactory factory)
      {
          this.factory = factory;
      }
   
      public CellPhone orderCellPhone(String style)
      {
          CellPhone cellphone;
   
          cellphone = factory.createCellPhone(style);
   
          cellphone.design(); //設計
          cellphone.modeling(); //建模
          cellphone.combine(); //組裝
          cellphone.box();//包裝
   
          return cellphone;
      }
  }

 

看到第三行,我們為CellPhoneSale加上一個變數,以便記錄CellPhoneFactory,同時在建構式的時候,將一個Factory的參數帶入(就類似於前幾個Design Pattern實作時的方式。

再看到第14行,我們不再使用具象實體化,而是把new代換成工廠的方法了!

 

上述就是簡單工廠(SimpleFactory)!!但很遺憾,這不是設計模式,這只是一種coding的習慣!常有人誤認這個就是工廠模式的精髓。不過不要因為簡單工廠不是一個真正的模式,就忽略了它的用法。先來看看新版本的手機店的類別圖。

 

2

 

客戶–>工廠–>產品(抽象)—>產品實踐。這樣的關係可以讓具象產品都必須實踐CellPhone的介面(抽象類別也算,這邊是泛指實踐某個超型態(可以是介面或類別)),無呰以來就可以被工廠建立並傳回給客戶。

各種型式的手機都可以override 超類別的方法,實踐自己的製作方式。

 

工廠模式完了嗎?

接下來登場的是兩個重量級的模式,然後他們都是工廠…而手機只會愈來愈多…,接著看吧!

 

事業愈做愈大,手機開發經營有成,擊敗了其他的手機開發商,現在大家都希望產業移植,到大陸開分公司處理大陸的手機銷售,而大陸也有一個製造工廠(地區性質不一定),去賺更多的錢。產業準備複製,不過除了我們,經營者也會開始擔心!到大陸設廠的品質會不會下降呢?,會不會搞糟了原本的品質水準呢?因此希望大陸那邊都能利用自己的程式碼,好讓手機製造的流程不會改變。這樣在未來甚至可以把所有的生產流程都移到大陸,不過剛開始嘛,台灣工廠專門快速回應台灣的客戶,而大陸客戶由大陸工廠負責快速回應製作手機。

剛剛好呢,已經有一個作法,如果利用CellPhoneFactory,寫出二種不同的工廠,分別是ChinaCellPhoneFactory以及TaiwanCellPhoneFactory,那麼各地的手機商,就都有適合的工廠可以使用。這是一種作法…來看看會變成什麼樣子!

  TaiwanCellPhoneFactory twFactory = new TaiwanCellPhoneFactory();
  CellPhoneSale twSale = new CellPhoneSale(twFactory);
  twSale.orderCellPhone("MusicPhone");
   
  ChinaCellPhoneFactory caFactory = new ChinaCellPhoneFactory();
  CellPhoneSale caSale = new CellPhoneSale(caFactory);
  caSale.orderCellPhone("MusicPhone");

透過建立台灣工廠,然後建立一個台灣銷售部來處理台灣的訂單,將台灣工廠放到建構式參數中,當製造手機時,就會產生台灣區的手機

然而在推動不同地區的工廠時,經營者漸漸地發現,兩岸三地的喜好差異不小,所以公司在大陸建立了一個經營團隊,專門負責大陸手機的販售

他們調查大陸的需求以後,發現手機除了簡繁體外,他們對於手機的系統、功能、組件、包裝,配件都有不同的需求,擁有屬於自己地區性的手機原型(需求產生的),而推廣工廠的時候,發現不同的手機銷售部門雖然是用我們的手機,但是在製造上仍有不同的作法。所以我們希望建立一個框架,把不同的區域性的建立手機原型、製造與銷售手機綑綁在一起,卻又不失彈性。在建立簡單工廠之前,製造手機的程式碼綁在CellPhone Sale中,但這麼做卻沒有彈性。那麼怎麼做才能魚與熊掌兼得呢?

 

給不同銷售地區使用的原型框架

嗯…有個作法可以讓區域差異的手機活躍於CellPhoneSales類別,同時讓這些不同地區的分公司,依然可以自由製作該區域的產品組合。

 

首先,先把createCellPhone放回CellPhoneSale之中,不過要設定成抽象方法,然後要將地區性的銷售寫一個CellPhoneSale的次類別

來看看變了哪些東西。

  public abstract class CellPhoneSale
   {
       public CellPhone orderCellPhone(String style)
       {
           CellPhone cellphone;
  
           cellphone = createCellPhone(style);
  
           cellphone.design(); //設計
           cellphone.modeling(); //建模
           cellphone.combine(); //組裝
           cellphone.box();//包裝
  
           return cellphone;
       }
  
       public abstract CellPhone createCellPhone(String style);
   }

現在CreateCellPhone被移回了CellPhoneSale,而且工廠方法必須宣告為抽象方法。CellPhoneSale類別要宣告成抽象的

因此現在已經有一個CellPhoneSale作為超類別,讓每個區域型態(Taiwan、China),都繼承這個CellPhoneSale,每個次類別各自決定如何製造手機。看看要如何進行吧

 

目前處理手機銷售類別已經有一個不錯的訂單系統,由orderCellPhone()方法負責處理訂單,不過在地區性差異上,是要讓createPhone能因應改變,我們希望對訂單的處理能夠一致。

而各區域手機銷售單位之間的差異,在於製作手機原型上的差異(例如台灣有較高階的商務手機2代也有娛樂手機,但大陸沒有娛樂手機,而有運動手機)。

,做法就是讓大陸與台灣的分公司各個次類別去定義自己的createCellPhone()方法,所以會有一些CellPhoneSale的具象次類別,每個次類別都有自己區域不同的需求差異。

這仍然適用於CellPhoneSale框架,和orderCellPhone合作無間。

3

新的定義是每個次類別都會推翻createCellPhone方法,具有自主權地去定義自己區域需求建立原型的方法,同時使用CellPhoneSale所定義的orderCellPhone的方法,甚至可以把orderCellPhone宣告成final,以防止被分公司推翻訂購流程。

而利用不同地區的次類別,createCellPhone方法就會建立不同區域性的手機。

關於這方面,從CellPhoneSale的orderCellPhone方法觀點來看,此方法被宣告在抽象的類別內,但是在具象的次類別中實踐,雖然orderCellPhone對CellPhone物件做了許多事

但是它『並』不知道,這個CellPhone物件的具象類別為何。這正是鬆綁的一種行為(decouple)!當orderCellPhone呼叫createCellPhone()的時候,不同區域次銷售單位類別將負責透過不同的需求建立手機原型。而當然是由不同區域各自決定手機的需求來建立原型。

雖然次類別不是即時做出這樣的決定,但是嚴格說來,是由不同區域性的手機客戶決定到買,而決定了手機的區域需求。

來建立分公司吧…

  public abstract class CellPhoneSale
  {
      public CellPhone orderCellPhone(String style)
      {
          CellPhone cellphone;
 
          cellphone = createCellPhone(style);
 
          cellphone.design(); //設計
          cellphone.modeling(); //建模
          cellphone.combine(); //組裝
          cellphone.box();//包裝
 
          return cellphone;
      }
 
      public abstract CellPhone createCellPhone(String style);
  }
 
 
 
 
  public class ChinaCellPhoneSale :  CellPhoneSale
  {
      public override CellPhone createCellPhone(String style)
      {
          if (style.Equals("Business"))
          {
              return new caBusinessPhone(); //第1代的商務手機
          }
          else if (style.Equals("Music"))
          {   //音樂手機
              return new caMusicPhone();
          }
          else if (style.Equals("Sport"))
          {  //1代的Sport手機
              return new caSportPhone();
          }
          else
          {
              return null;
          }
      }
  }
 
  public class TaiwanCellPhoneSale : CellPhoneSale
  {
      public override CellPhone createCellPhone(String style)
      {
          if (style.Equals("Business2"))
          {
              return new twBusiness2Phone(); //第2代的商務手機
          }
          else if (style.Equals("Music"))
          {   //音樂手機
              return new twMusicPhone();
          }
          else if (style.Equals("Entertainment"))
          {  //娛樂手機
              return new twEntertainmentPhone();
          }
          else
          {
              return null;
          }
      }
  }

靠近一點來看…是在何時宣告了工廠方法?答案是…

abstract CellPhone createCellPhone(String style);

這一行專門處理物件的建立,並將這樣的行為包裝起來,放在次類別中。走類別的程式碼(orderCellPhone)會用到次類別的程式碼(createCellPhone),但是藉由工廠方法,超類別和次類別之間的關係被鬆綁了呢。

 

前面講了這麼久,有沒有發現一直都忽略了手機本身?如果手機本身沒有定義,那有店也賣不了東西。現在就來實踐手機:

  class CellPhone
  {
      //每款手機都有名稱、硬體規格(簡化)、作業系統軟體(簡化)
      public string Name;
      public string Hardware;
      public string OS;
 
      //總公司規定的基本流程。
      public void design()
      {
          Console.WriteLine("doing Design Process....");
      }
 
      public void modeling()
      {
          Console.WriteLine("doing Modeling Process....");
      }
 
      public void combine()
      {
          Console.WriteLine("doing Combine Process....");
      }
 
      public void box()
      {
          Console.WriteLine("doing Boxing Process....");
      }
 
      public string getName()
      {
          return Name;
      }
  }
 
  class caBusinessPhone : CellPhone
  {
      public caBusinessPhone()
      {
          base.Name = "China Business CellPhone";
          base.OS = "Android 1.5";
          base.Hardware = "電阻式營幕";
      }
 
      public override void box()
      {
          //大陸分公司銷售單位認為要推翻了原本的包裝方式,採用大陸的包裝流程,符合他們的風格。
          Console.WriteLine("doing China Boxing Process....");
      }
 
  }
 
  class twBusiness2Phone : CellPhone
  {
      public twBusiness2Phone()
      {
          base.Name = "Taiwan Business2 SmartPhone";
          base.OS = "Android 2.0";
          base.Hardware = "電容式營幕";
      }
  }

其他完整的程式則不實作,請以上類推即可。

等很久了嗎?手機該上市啦!來製造一些手機吧!

  class Program
  {
      static void Main(string[] args)
      {
          //建立兩間不同的分公司
          CellPhoneSale TaiwanSale = new TaiwanCellPhoneSale();
          CellPhoneSale ChinaSale = new ChinaCellPhoneSale();
  
          //
          CellPhone cellphone = TaiwanSale.orderCellPhone("Business2");
          Console.WriteLine("gipi 訂了一個" + cellphone.getName() + "手機!");
  
  
          CellPhone cellphone2 = ChinaSale.orderCellPhone("Business");
          Console.WriteLine("pou 訂了一個" + cellphone2.getName() + "手機!");
  
          Console.ReadKey();
      }
  }

是的,執行以後呢?

4

 

終於該來定義工廠模式了!當然假如都看到這邊了,你一定有一些想法:

工廠模式會把物件建立的過程封裝起來,來看看類別圖:

3

建立者(Creator)類別,其中能夠產生產品的類別稱為具象建立者。而當中的createCellPhone方法(具象類別中的哦),正是工廠方法,用來製造產品

 

而另外就是產品類別,CellPhone定義了手機,然後實際將要製造出來的手機原型都會繼承他。(像business、business2型的手機)

其實我們看到一個orderCellPhone將一個工廠方法聯合起來,然而這也可以視為是一種平行框架,他們同樣都是由抽象類別,再去實作許多具象的次類別

而每個次類別都有自己特定的實踐方式。TaiwanCellPhoneSale就是封裝台灣區域性手機的知識,大陸地區性手機的知識則由ChinaCellPhoneSale來封裝。

 

深入淺出一書定義:Factory Method Design Pattern (工廠方法模式)

定義了一個建立物件的介面,但由次類別決定要實體化的類別為何者,工廠方法讓類別把實體化的動作交由次類別進行。

5

為什麼常常會誤解呢?工廠方法的模式能夠封裝具象型態的實體化。如上圖,任何其它方法,都可能使用到這個工廠方法所製造出來的產品

但只有次類別真正實現了這個工廠方法,產生出物件。

常常有人會說,工廠方法讓次類別決定要實體化的類別為何者,但這邊所謂的決定並不是指模式允許次類別本身在執行期做決定。

而是指建立者類別的程式碼在撰寫時,不需要知道實際建立的產品是何者,選擇了使用哪個次類別,自然就決定了實際建立的產品為何了。

而要搞清楚的是,我們建立的大陸與台灣的分公司(TaiwanSale、ChinaSale),究竟與利用簡單工廠有何不同?它們真的很類似,但,用法不一樣。

雖然每個具象分公司的實踐看起來很像是簡單工廠,但是這裡工廠方法模式必須是繼承自一個類別,此類別有一個抽象方法createCellPhone()。

再由每個分公司自行負責createCellPhone()方法的行為。在簡單工廠中,工廠是另一個物件,由超類別CellPhoneSales所使用。

 

還不清楚嗎?次類別(TaiwanCellPhoneSale、ChinaCellPhoneSale)看起來真的很象簡單工廠吧?,簡單工廠不同的是,它把全部的事情都在一個地方處理掉了

然而工廠方法是建立一個框架,讓次類別決定要如何實踐。比如說好了,在工廠方法中orderCellPhone方法提供了一般的框架,以建立手機原型。

orderCellPhone依賴工廠方法建立具象類別,並產生出實際的手機,可以由繼承CellPhoneSale類別,決定實際製造出的手機是哪種區域特色。

簡單工廠的做法可以將物件的建立封裝起來,但是不具備工廠方法的彈性,因為簡單工廠不能改變正在建立的產品!

 

本例中採用了台灣跟大陸的區域性,既使只有一個concreteCreator,工廠方法模式仍然很有用,因為我們將產品的實踐與使用予以decouple(鬆綁)

如果增加了產品的實踐,Creator並不會受到影響,因為Creator與ConcreteProduct之間並沒有緊密的結合綑綁。

 

工廠將建立物件的程式碼封裝集中在一個物件或行為中,可以避免程式碼的重複,並更方便以後的修改,

也意味在進行物件實體化時,只會用到介面,而不再依賴具象類別。這正是幫助針對介面寫程式,而不是針對實踐寫程式。

然而,在工廠方法模式之中,不可避免的仍然必須使用具象類別,實體化真正的物件不是嗎?

 

事實上還有相依性的謎題沒有解呢…

 

我們常直接實體化一個物件,就是在依賴具象類別,如同本最一開始的角色扮演案例,或是手機分公司案例

假如不知道工廠方法模式的話,那就會在一個類別中去new新物件,去實體化所有的手機或是角色物件,而不是委由工廠製造。

以手機分公司為例,假如一個手機分公司完全依賴所有的手機物件(因為直接在CellPhoneSale類別去建立手機),那麼一旦類別的實踐方式改變了,就要去

修改這個手機分公司的程式。這正是依賴具象類別(產品類別)的實例,相依性如下圖。

6

 

本書精華來了…

OO守則:請依賴抽象類別,不要依賴具象類別!

這有一個專有名詞叫dependency Inversion Principle。

這很酷,程式碼減少對於具象類別的依賴是件好事,這守則聽起來滿像請對介面寫程式那個守則,然而,本守則是指不要讓高階元件(公司)依賴低階元件(產品)

不論高低階,兩者都應該相依於抽象類別!

所以這樣的相依性該如何解決呢?回想一下工廠方法,針對產品去建立一個抽象的類別,再由產品的具象類別去實作各自的方法

7

看看上面,目前高階元件(CellPhoneSale,以及這些手機(caMusicPhone、twBusiness2Phone等)都依賴了CellPhone抽象,而遵循這個OO守則

工廠方法模式正是最有威力的技巧之一。

那為什麼會有Inversion呢?走一次我們思考問題的流程吧!以書中的例子為例,假如現在要開一間pizza店

你第一個會想到的就是pizza店,那產品是什麼呢?因此要做出來的產品就是pizza,但是你不想讓pizza店依賴pizza產品,不然將全部依賴具象類別。

所以要開始反轉,先從pizza開始,能抽象成什麼類別呢?

對!就是pizza(抽象)類別,那要這麼做,必須靠一個工廠來取出這些pizza的具象類別,一旦這樣做,各種具象pizza的產品就只能依賴一個抽象了。

那pizza店呢?也會依賴那個抽象。

 

關於dependency Inversion Principle有一些指導方針(見原書)

1.變數不可以持有具象類別的參考(如果使用new,就會持有具象類別的參考,這時請改用工廠避開)

2.不要讓類別繼承自具象類別(請繼承自一個抽象或介面)

3.不要讓次類別中的方法override超類別中的方法。(如果這樣,就不是一個真正適合被繼承的抽象)

要盡量去達到,而不是隨時都要遵行,有趣的是我們常常直接實體化具象類別,那就是字串物件(字串型態是參考型別),但這個合理嗎?

很合理,因為字串不會改變(假如我們將指派新的字串時,其實只是將原本的指標指向新的字串物件的位址)

 

後記:工廠模式還沒完,本篇依然延續先前,在閱讀過深入淺出設計模式一書後,以C#實作程式碼並重新構思案例來輔佐自己理解,工廠模式是個精華

要吸收要反覆地閱讀與體會,不過很高興又學會了一個設計模式。然而為什麼工廠模式還沒完?因為還有一個抽象工廠模式等著我。

不過這波未平,先別躁進,能體會出本工廠方法模式以後,再進到下一個模式也永遠不遲呢。玩玩看吧。