抽象工廠模式筆記

抽象工廠模式筆記

註:定義來自於深入淺出設計模式一書,本例重新構思案例。

還記得http://www.dotblogs.com.tw/pin0513/archive/2010/01/12/12963.aspx

製造手機的工廠吧?

 

手機公司的設計變的很棒,導入工廠模式以後,具有了彈性的框架,而且遵循設計守則!

 

還記得台灣分公司與大陸分公司在成立的時候都是繼承著CellPhoneSale,這個類別中定義著所有分公司

的手機製造與包裝的流程,orderCellPhone方法中定義著所有公司都要遵循的訂購流程,但是製造則由CellPhoneSale的具象類別去實踐

因為地區需求的差異,所以各具象類別去override超類別中的製造方法,實踐不同地區需求的各個手機製造。

 

這個從台灣發跡的手機品牌,因為台灣手機的設計與製造的優良名聲

採用了最新的科技、品質良好而且穩定的生產組件,讓台灣在世界舞台的高科技生產上具有一席之地。

 

因為大陸分公司的經營團隊在大陸設計與生產手機,因此採用大陸的零件來生產手機

雖然可以藉此來獲得他們分公司更高的利潤,但是總公司開始擔心了

因為大陸有些品質不良或是不明的零件,可能都是一個傷害這個手機品牌的風險。

因此總公司現階段必須採取一些手段,以免未來毀了這個剛起步的手機品牌。

 

1.如何確保每個分公司都使用統一生產的手機組件?

想當然爾,就是先由總公司設立專門處理手機組件的工廠,並將組件運送到各地區的分公司的製造單位去。

對於這個做法,還有一個問題跟地區差異有關:

1

各地區因應區域的需求與分公司的決策,兩個地方的手機組件是不一樣的,例如作業系統、硬體的差異等等。

雖然最終都組成手機賣出,但是因應不同地區的使用對象,內涵有著不一樣設計的差異。

本例中將現實狀況簡化,假設產品組件主要為軟體的作業系統(OS)、手機硬體:營幕、USB插口以及3G模組

 

例如,大陸地區因為google的退出,因此大陸的手機全面採用WM的系統(會不會太政治化> <)。

而台灣的手機全面推出Android系統的手機(策略)。

 

因此現在,我們不能只有產品組件家族的定義(軟、硬體),當大陸使用一組組件,而台灣使用另一組組件

那未來歐洲分公司即將開啟呢?可能不久之後,大陸分公司還要分成內陸分公司,專門處理不同省份的手機生產與銷售業務。

那該怎麼辦?

 

想要行的通?必須先清楚如何處理組件的家族。

2

就上圖而言,所有的手機都是相同的元件製造而成的,但是每個區域的分公司對於這些元件有不同的實踐方式。

整體來說,這幾個區域編造了組件家族,每區域實踐了一個完整的組件家族。

 

 

 

你現在知道我們要做些什麼了?

總公司出錢要建立一個組件工廠了(設在馬來西亞)(現實中,營幕或是其他零件可能都是專業廠商在處理”生產”的

這邊的工廠不是真的製造所有零件,而是透過統一的組件工廠來處理各個手機的組件的準備。)

至少這邊我們可以確認由總公司建立的組件組裝工廠,可以幫我們確認手機的品質與測試。

這個組件工廠將負責生產組件家族中的每一種組件,也就是說工廠將需要處理手機的軟體安裝以及硬體的組成,待會兒

我們再處理各區域組件的差異。

 

我們先為工廠定義一個介面吧!這個介面負責準備所有的組件:

   1: public interface CellPhoneAssemblyFactory
   2: {
   3:     //在介面中每個組件都有一個對應的方法來準備該組件
   4:     OS installOS();
   5:     Monitor prepareMonitor();
   6:     USB prepareUSB();
   7:     Wireless3G prepareWireless3G();
   8:     //看到上面有許多新類別,每個組件都是一個類別
   9: }

 

下列程式碼是接下來實驗會用到的組件,先建立好,在現實中,這邊的程式碼可以透過與相關的廠商建立通道

當需要哪一個組件的時候,就可以透過某家廠商來進貨。

   1: //組件類別
   2:     public class OS
   3:     {
   4:        public string partName;      
   5:     }
   6:  
   7:     public class Android:OS
   8:     {
   9:         public Android()  //系統
  10:         {
  11:           base.partName = "Android 2.1"; 
  12:         }
  13:  
  14:         public Android(string type)  //系統語言
  15:         {
  16:             //這只是一個例子,相關的動態行為都可以設計到類別中
  17:             if (type = "繁體")
  18:                 base.partName = "Android 2.1(繁)";
  19:             else
  20:                 base.partName = "Android 2.1(簡)";
  21:         }          
  22:     }
  23:  
  24:  
  25:     public class WindowsMobile:OS
  26:     {
  27:         public WindowsMobile()  //系統
  28:         {
  29:           base.partName = "WindowsMobile 6.5"; 
  30:         }
  31:  
  32:         public WindowsMobile(string type)  //系統語言
  33:         {
  34:             //這只是一個例子,相關的動態行為都可以設計到類別中
  35:             if (type = "繁體")
  36:                 base.partName = "WindowsMobile 6.5(繁)";
  37:             else
  38:                 base.partName = "WindowsMobile 6.0(簡)";
  39:         }        
  40:     }
  41:  
  42:  
  43:  
  44:     public class Monitor
  45:     {
  46:        public string partName;   
  47:     }
  48:  
  49:     public class capacitiveMonitor:Monitor{
  50:         public capacitiveMonitor()  //電容式
  51:         {
  52:           base.partName = "CapacitiveMonitor電容式"; 
  53:         }
  54:     }
  55:  
  56:     public class resistanceMonitor:Monitor{
  57:         public resistanceMonitor()  //電阻式
  58:         {
  59:           base.partName = "ResistanceMonitor電阻式"; 
  60:         }
  61:     }
  62:     
  63:     public class USB
  64:     {
  65:        public string partName;   
  66:     }
  67:  
  68:  
  69:     public class normalUSB:USB
  70:     {
  71:         public normalUSB()
  72:         {
  73:           base.partName = "一般USB插口";
  74:         }
  75:     }
  76:  
  77:     public class miniUSB:USB
  78:     {
  79:         public miniUSB()
  80:         {
  81:           base.partName = "miniUSB插口";
  82:         }
  83:     }
  84:  
  85:     public class Wireless3G
  86:     {
  87:        public string partName;   
  88:     }
  89:  
  90:     public class WCDMA:Wireless3G
  91:     {
  92:         public WCDMA()
  93:         {
  94:            base.partName = "WCDMA 3g模組";
  95:         }
  96:     }
  97:  
  98:     public class TD_SCDMA:Wireless3G
  99:     {
 100:         public TD_SCDMA()
 101:         {
 102:            base.partName = "TD-SCDMA 3g模組";
 103:         }
 104:     }

 

OK,接著要做的事情是:

1.為每一個區域建造一個工廠。需要建立繼承自CellPhoneAssemblyFactory的一個次類別,實踐每一個install、prepare方法

2.實踐一組組件類別供工廠使用,例如miniUSB、AndroidOS、TD-SCDMA_3G。這些類別可以合宜地被不同區域供用。

3.然後仍然需要將這一切組織起來,將新的組件工廠整合進舊的手機銷售程式碼中。

 

ok,先來建立台灣手機的組件工廠吧。

   1: public class twCellPhoneAssemblyFactory : CellPhoneAssemblyFactory 
   2: {
   3:     public OS installOS()
   4:     {
   5:         return new Android("繁體");
   6:     }
   7:  
   8:     public Monitor prepareMonitor()
   9:     {
  10:         return new capacitiveMonitor();
  11:     }
  12:  
  13:     public USB prepareUSB()
  14:     {
  15:         return new miniUSB();
  16:     }
  17:  
  18:     public Wireless3G prepareWireless3G()
  19:     {
  20:         return new WCDMA();
  21:     }
  22: }

上述組件工廠對於組件家族內的每個組件都提供了台灣版本的組件。

例如台灣除了亞太是採用CDMA2000,大部分的電信公司是採用WCDMA的3g模組,所以總公司在台灣推出的手機的3g模組是採用WCDMA協定的模組。

而大陸版本的相信你知道應該怎麼做!

   1: public class caCellPhoneAssemblyFactory : CellPhoneAssemblyFactory
   2: {
   3:     public OS installOS()
   4:     {
   5:         return new WindowsMobile("簡體");
   6:     }
   7:  
   8:     public Monitor prepareMonitor()
   9:     {
  10:         return new resistanceMonitor();
  11:     }
  12:  
  13:     public USB prepareUSB()
  14:     {
  15:         return new normalUSB();
  16:     }
  17:  
  18:     public Wireless3G prepareWireless3G()
  19:     {
  20:         return new TD_SCDMA();
  21:     }
  22: }

工廠一切就緒,準備處理品質控管組件,現在只要來修改手機類別,好讓他們只使用工廠準備的組件。

原本我們在手機類別中定義跟下面很像,先從抽象手機(CellPhone)類別開始:

   1: public abstract class CellPhone
   2: {
   3:     //每款手機都有名稱、硬體規格(簡化)、作業系統軟體(簡化)
   4:     //public string Hardware;
   5:     //public string OS;
   6:  
   7:     public string Name{get;set;}
   8:     
   9:     
  10:     //每個手機使用一組組件,在組合階段會使用到。
  11:     protected OS os;
  12:     protected Monitor monitor;
  13:     protected USB usb;
  14:     protected Wireless3G wireless3G;
  15:  
  16:     
  17:     //總公司規定的基本流程。 //維持不變
  18:     public virtual void design()
  19:     {
  20:         Console.WriteLine("doing Design Process....");
  21:     }
  22:  
  23:     //維持不變
  24:     public virtual void modeling()
  25:     {
  26:         Console.WriteLine("doing Modeling Process....");
  27:     }
  28:  
  29:     //其他流程維持不變,只有這邊需要改變,我們增加一個準備方法
  30:     //現在把prepareAssembly()方法宣告成抽象。在這個方法中,需要收集手機所需要的組件
  31:     public abstract void prepareAssembly();
  32:  
  33:  
  34:     //維持不變
  35:     public virtual void combine()
  36:     {
  37:         Console.WriteLine("doing combine Process....");
  38:     }
  39:  
  40:     //維持不變
  41:     public virtual void box()
  42:     {
  43:         Console.WriteLine("doing Boxing Process....");
  44:     }
  45:  
  46:     public string getName()
  47:     {
  48:         return Name;
  49:     }
  50: }

比較大的改變如下:

第一個是整個手機類別我們把他宣告成抽象類別。

第二個是第11行開始,我們開始定義組件的取得與準備,因此我們要有這些專門放置組件的物件位置。

另一個部分是第31行,在組裝之前,我們要準備組件,這邊我們需要將準備方法宣告成抽象。(假如這個方法要為抽象方法,其類別也必須為抽象類別。)

而其他地方我們維持不變。

 

手機類別還沒修改完成呢…

如今有一個抽象手機,可以開始建立台灣與大陸的手機了,從此以後,分公司必須直接從工廠取得認可的組件,那些可能的風險(使用低品質的組件)暫時消失了。

 

我們曾經寫過工廠方法的程式碼,擁有twBusiness2Phone(二代商用手機)以及caBusinessPhone(一代商用手機)。比較一下這兩個類別

唯一的差別是在於使用區域性所需的組件,其他的作法都是一樣的。

以下是前一篇工廠方法的舊做法:

   1: class caBusinessPhone : CellPhone
   2: {
   3:     public caBusinessPhone()
   4:     {
   5:         base.Name = "China Business CellPhone";
   6:         base.OS = "Android 1.5";
   7:         base.Hardware = "電阻式營幕";
   8:           }
   9:  }
  10:  
  11: class twBusiness2Phone : CellPhone
  12: {
  13:         public twBusiness2Phone()
  14:         {
  15:         base.Name = "Taiwan Business2 SmartPhone";
  16:         base.OS = "Android 2.0";
  17:         base.Hardware = "電容式營幕";
  18:          }
  19: }

因此我們以twBusiness2Phone為例:

   1: class twBusiness2Phone : CellPhone
   2:  {
   3:      CellPhoneAssemblyFactory cellPhoneAssemblyFactory;
   4:      public twBusiness2Phone(CellPhoneAssemblyFactory cellPhoneAssemblyFactory)
   5:      {
   6:          //要組裝(Combine)手機,需要工廠提供組件。所以每個手機類別都有一個組件工廠
   7:          //需要從建構式得到一個工廠,記錄在實體變數中。
   8:          this.cellPhoneAssemblyFactory = cellPhoneAssemblyFactory;
   9:      }
  10:  
  11:      public override void prepareAssembly()
  12:      {
  13:          //神奇的事情發生在這邊
  14:          Console.WriteLine("Preparing Assembly:"+Name);
  15:          os = cellPhoneAssemblyFactory.installOS();
  16:          monitor = cellPhoneAssemblyFactory.prepareMonitor();
  17:          usb = cellPhoneAssemblyFactory.prepareUSB();
  18:          wireless3G = cellPhoneAssemblyFactory.prepareWireless3G();
  19:      }
  20:  }

從上面可以看到:

15: os = cellPhoneAssemblyFactory.installOS();

所產生的組件依賴所使用的工廠,而手機類別根本不在乎這些組件,他只知道如何製作手機。現在,CellPhone和區域組件之間被鬆綁,無論

組件工廠有哪些,未來就算有歐洲工廠,CellPhone類別都可以輕易地重複利用,完全沒問題。

也來看看大陸的caBusinessPhone手機:

   1: class caBusinessPhone : CellPhone
   2: {
   3:     CellPhoneAssemblyFactory cellPhoneAssemblyFactory;
   4:     public caBusinessPhone(CellPhoneAssemblyFactory cellPhoneAssemblyFactory)
   5:     {
   6:         //要組裝(Combine)手機,需要工廠提供組件。所以每個手機類別都有一個組件工廠
   7:         //需要從建構式得到一個工廠,記錄在實體變數中。
   8:         this.cellPhoneAssemblyFactory = cellPhoneAssemblyFactory;
   9:     }
  10:  
  11:     public override void prepareAssembly()
  12:     {
  13:         //神奇的事情發生在這邊
  14:         Console.WriteLine("Preparing Assembly:"+Name);
  15:         os = cellPhoneAssemblyFactory.installOS();
  16:         monitor = cellPhoneAssemblyFactory.prepareMonitor();
  17:         usb = cellPhoneAssemblyFactory.prepareUSB();
  18:         wireless3G = cellPhoneAssemblyFactory.prepareWireless3G();
  19:     }
  20: }

ok,再回到手機銷售的時刻:

   1: public abstract class CellPhoneSale
   2: {
   3:     public CellPhone orderCellPhone(String style)
   4:     {
   5:         CellPhone cellphone;
   6:  
   7:         cellphone = createCellPhone(style);
   8:  
   9:         cellphone.design(); //設計
  10:         cellphone.modeling(); //建模
  11:  
  12:         //新增的方法
  13:         cellphone.prepareAssembly();
  14:         cellphone.combine(); //組裝
  15:         cellphone.box();//包裝
  16:  
  17:         return cellphone;
  18:     }
  19:  
  20:     public abstract CellPhone createCellPhone(String style);
  21: }
  22:  
  23: public class ChinaCellPhoneSale :  CellPhoneSale
  24: {
  25:     public override CellPhone createCellPhone(String style)
  26:     {
  27:         CellPhone cellphone = null;
  28:         //大陸分公司會用到大陸手機組件工廠,由該組件工廠負責處理大陸用的手機組件
  29:  
  30:  
  31:         if (style.Equals("Business"))
  32:         {
  33:             cellphone = new caBusinessPhone(new caCellPhoneAssemblyFactory()); //把工廠傳給每一個手機,以便手機可從工廠取得組件
  34:             cellphone.Name = "第一代音樂手機";
  35:         }
  36:         else if (style.Equals("Music")) 
  37:         {
  38:             //音樂手機類別記得要實踐(如caBusinessPhone)
  39:             //Code here 
  40:         }
  41:         else if (style.Equals("Sport"))
  42:         {
  43:             //運動手機類別記得要實踐
  44:             //Code here
  45:         }
  46:         else
  47:         {
  48:             return null;
  49:         }
  50:  
  51:         return cellphone;
  52:     }
  53: }
  54:  
  55: public class TaiwanCellPhoneSale : CellPhoneSale
  56: {
  57:     public override CellPhone createCellPhone(String style)
  58:     {
  59:         CellPhone cellphone = null;
  60:         //大陸分公司會用到大陸手機組件工廠,由該組件工廠負責處理大陸用的手機組件
  61:         CellPhoneAssemblyFactory cellPhoneAssemblyFactory = new twCellPhoneAssemblyFactory();
  62:  
  63:         if (style.Equals("Business2"))
  64:         {
  65:             cellphone = new twBusiness2Phone(new twCellPhoneAssemblyFactory()); //把工廠傳給每一個手機,以便手機可從工廠取得組件
  66:             cellphone.Name = "第二代音樂手機";
  67:         }
  68:         else if (style.Equals("Music"))
  69:         {   //音樂手機記得要實踐程式碼(如twBusiness2Phone)
  70:             //Code here
  71:         }
  72:         else if (style.Equals("Entertainment"))
  73:         {  
  74:             //娛樂手機記得要實踐
  75:             //Code here
  76:         }
  77:         else
  78:         {
  79:             return null;
  80:         }
  81:         return cellphone;
  82:     }
  83: }

註:這邊新增了一個prepareAssembly()方法,因此在超類別orderCellPhone的流程中,要補進去喲(第12行),本程式碼範例僅實作台陸商用手機,音樂、娛樂手機,請自己想想看吧。

 

ok,接著到各銷售部門去巡視一下,確認他們使用了正確的組件工廠與手機,而對於每個種類的手機

一律實體化一個新的手機,並傳進該種手機的組件工廠,以便手機取得組件

image

很明顯,雖然我們這邊多了一個流程,但是架構上更彈性了!

 

我們做了什麼事?

這一連串的程式碼改變,在此導入了新型態的工廠,也就是所謂的抽象工廠,用來建立手機組件的家族。

透過抽象工廠的介面(別忘了,是CellPhoneAssemblyFactory) ,可以建立產品的家族(CellPhone)

利用這個介面寫的程式,把實際製造產品的工廠鬆綁(例如前一篇的工廠方法twBusiness2Phone的建構式)

以便實踐各式各樣的工廠,製造出各種不同的產品,不同的區域、不同的作業系統、不同的USB介面,3g模組。

 

因為程式decoupling(記得嗎?鬆綁)了實際的手機,所以可以替換成不同的工廠,取得不同的行為。

3

Main()的程式解說:

我們建立台灣分公司   

   1: CellPhoneSale TaiwanSale = new TaiwanCellPhoneSale();

現在已經可以接受訂單了

   1: CellPhone cellphone = TaiwanSale.orderCellPhone("Business2");

我們看到orderCellPhone()方法,他會呼叫TaiwanSale中的一個orderCellPhone的方法,而這個方法裡面首先先呼叫

   1: cellphone = createCellPhone(style)方法

接下來就不一樣了,因為createCellPhone方法會開始牽扯到組件工廠。

   1: cellphone = new twBusiness2Phone(new twCellPhoneAssemblyFactory());  

把TW工廠傳給手機,以便手機可從工廠取得台灣組件

接下來需要準備組件了,一旦呼叫了prepareAssembly方法,工廠將被要求準備組件

   1: public override void prepareAssembly()
   2: {
   3:     os = cellPhoneAssemblyFactory.installOS();
   4:     monitor = cellPhoneAssemblyFactory.prepareMonitor();
   5:     usb = cellPhoneAssemblyFactory.prepareUSB();
   6:     wireless3G = cellPhoneAssemblyFactory.prepareWireless3G();
   7: }

最後,有了準備好的手機組件,就會接著combine、box,然後傳給顧客手上囉

 

我們來定義一下抽象工廠模式:

抽象工廠模式

提供了一個介面,建立相關或相依物件之家族,而不需要明確指定具象類別(摘自深入淺出設計模式一書)

抽象工廠模式允許客戶使用抽象的介面,建立一組相關的產品,而不需要知道(或關心實際產出的具象產品為何。如此一來,客戶就從具象的產品中被鬆綁了。

看看抽象工廠的類別圖:(摘自深入淺出設計模式一書)

4

這可謂相當的複雜,但其實我們從分公司的角度來看吧:

從手機分公司的兩個實體,分別是手機組件廠(抽象工廠)的客戶。

而抽象工廠是一個定義了如何產生一個關聯產品的家族。這個家族包含了所有製作手機的組件。

以台灣的組件工廠而言,具象的台灣工廠負責產生準備台灣手機的組件,如何產生符合自己區域的正確物件。

而右邊是對於產品家族,每個工廠都有不同的實踐方式!!

 

假如你觀察到了,你會問:工廠方法是不是潛伏在抽象工廠裡面呢?(prepareUSB、installOS…etc不都是宣告成抽象,而次類別來推翻實踐嗎?)

抽象工廠確實經常以工廠方法的方式實踐,這很有道理,抽象工廠的任務是定義一個介面,而這個介面是負責建立一個具象產品,

同時利用實踐抽象工廠的次類別,提供這些具體作法。所以在抽象工廠中,利用工廠方法實踐產品方法,是很自然的。

 

但你會發現工廠方法和抽象工廠都是負責建立物件,但是工廠方法是透過繼承的方式,而抽象工廠是透過物件合成。

所以這意味著工廠方法建立物件必須去繼承一個類別,然後推翻他的工廠方法。

 

整個工廠模式不外乎就是藉由繼承來的次類別建立物件。而客戶只需要知道抽象的類別就好,由次類別負責決定具象型態。

工廠模式只負責將客戶從具象型態中鬆綁。

抽象工廠也做的到這一點,只是做法不同:

抽象工廠提供了一個抽象型態,用來建立一個產品家族。這個型態的次類別定義了產品被產生的方法。欲使用這個工廠,必須先實體化一個工廠

然後傳進一些針對抽象型態寫的程式。而家族的好處即是將一群相關的產品集結起來。

不過抽象工廠的問題是,如果家族加入了新產品,就必須改變介面…從規模而言。

工廠方法只需要一個方法,僅是建立一個產品。但抽象工廠需要一個大的介面,因為他要被用來建立整個產品家族。

所以每當需要建立產品家族以及讓製造的相關產品集結起來時,請選用抽象工廠。

而當你想要單純將客戶程式碼從需要被實體化的具象類別中鬆綁。或著是目前不知道將來需要實體化哪些具象類別的時候

就可以使用工廠方法。只要繼承工廠方法成為次類別,並實踐就好了。

 

知道使用時機了嗎?

 

後記:深入淺出設計模式,這本書從工廠模式定義了這兩個:工廠方法與抽象工廠模式,這兩個模式都是將物件建立的過程封裝起來,以便鬆綁程式碼與具象的類別

過完了這一章,也學到了設計模式的一個里程碑之一,工廠方法非常常被用到,像是啊.NET Framework中的物件建立很多都是透過工廠方法。

而像Flash中很多UI的物件定義也都是透過工廠模式來定義。所以在學習架構的設計上,工廠方法應該要致力去學會,你將會非常非常常看到它。

 

下一章預告獨體模式(Singleton DesingPattern)。