抽象工廠模式筆記
註:定義來自於深入淺出設計模式一書,本例重新構思案例。
還記得http://www.dotblogs.com.tw/pin0513/archive/2010/01/12/12963.aspx
製造手機的工廠吧?
手機公司的設計變的很棒,導入工廠模式以後,具有了彈性的框架,而且遵循設計守則!
還記得台灣分公司與大陸分公司在成立的時候都是繼承著CellPhoneSale,這個類別中定義著所有分公司
的手機製造與包裝的流程,orderCellPhone方法中定義著所有公司都要遵循的訂購流程,但是製造則由CellPhoneSale的具象類別去實踐
因為地區需求的差異,所以各具象類別去override超類別中的製造方法,實踐不同地區需求的各個手機製造。
這個從台灣發跡的手機品牌,因為台灣手機的設計與製造的優良名聲
採用了最新的科技、品質良好而且穩定的生產組件,讓台灣在世界舞台的高科技生產上具有一席之地。
因為大陸分公司的經營團隊在大陸設計與生產手機,因此採用大陸的零件來生產手機
雖然可以藉此來獲得他們分公司更高的利潤,但是總公司開始擔心了
因為大陸有些品質不良或是不明的零件,可能都是一個傷害這個手機品牌的風險。
因此總公司現階段必須採取一些手段,以免未來毀了這個剛起步的手機品牌。
1.如何確保每個分公司都使用統一生產的手機組件?
想當然爾,就是先由總公司設立專門處理手機組件的工廠,並將組件運送到各地區的分公司的製造單位去。
對於這個做法,還有一個問題跟地區差異有關:
各地區因應區域的需求與分公司的決策,兩個地方的手機組件是不一樣的,例如作業系統、硬體的差異等等。
雖然最終都組成手機賣出,但是因應不同地區的使用對象,內涵有著不一樣設計的差異。
本例中將現實狀況簡化,假設產品組件主要為軟體的作業系統(OS)、手機硬體:營幕、USB插口以及3G模組
例如,大陸地區因為google的退出,因此大陸的手機全面採用WM的系統(會不會太政治化> <)。
而台灣的手機全面推出Android系統的手機(策略)。
因此現在,我們不能只有產品組件家族的定義(軟、硬體),當大陸使用一組組件,而台灣使用另一組組件
那未來歐洲分公司即將開啟呢?可能不久之後,大陸分公司還要分成內陸分公司,專門處理不同省份的手機生產與銷售業務。
那該怎麼辦?
想要行的通?必須先清楚如何處理組件的家族。
就上圖而言,所有的手機都是相同的元件製造而成的,但是每個區域的分公司對於這些元件有不同的實踐方式。
整體來說,這幾個區域編造了組件家族,每區域實踐了一個完整的組件家族。
你現在知道我們要做些什麼了?
總公司出錢要建立一個組件工廠了(設在馬來西亞)(現實中,營幕或是其他零件可能都是專業廠商在處理”生產”的
這邊的工廠不是真的製造所有零件,而是透過統一的組件工廠來處理各個手機的組件的準備。)
至少這邊我們可以確認由總公司建立的組件組裝工廠,可以幫我們確認手機的品質與測試。
這個組件工廠將負責生產組件家族中的每一種組件,也就是說工廠將需要處理手機的軟體安裝以及硬體的組成,待會兒
我們再處理各區域組件的差異。
我們先為工廠定義一個介面吧!這個介面負責準備所有的組件:
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,接著到各銷售部門去巡視一下,確認他們使用了正確的組件工廠與手機,而對於每個種類的手機
一律實體化一個新的手機,並傳進該種手機的組件工廠,以便手機取得組件
很明顯,雖然我們這邊多了一個流程,但是架構上更彈性了!
我們做了什麼事?
這一連串的程式碼改變,在此導入了新型態的工廠,也就是所謂的抽象工廠,用來建立手機組件的家族。
透過抽象工廠的介面(別忘了,是CellPhoneAssemblyFactory) ,可以建立產品的家族(CellPhone)
利用這個介面寫的程式,把實際製造產品的工廠鬆綁(例如前一篇的工廠方法twBusiness2Phone的建構式)
以便實踐各式各樣的工廠,製造出各種不同的產品,不同的區域、不同的作業系統、不同的USB介面,3g模組。
因為程式decoupling(記得嗎?鬆綁)了實際的手機,所以可以替換成不同的工廠,取得不同的行為。
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,然後傳給顧客手上囉
我們來定義一下抽象工廠模式:
抽象工廠模式
提供了一個介面,建立相關或相依物件之家族,而不需要明確指定具象類別(摘自深入淺出設計模式一書)
抽象工廠模式允許客戶使用抽象的介面,建立一組相關的產品,而不需要知道(或關心實際產出的具象產品為何。如此一來,客戶就從具象的產品中被鬆綁了。
看看抽象工廠的類別圖:(摘自深入淺出設計模式一書)
這可謂相當的複雜,但其實我們從分公司的角度來看吧:
從手機分公司的兩個實體,分別是手機組件廠(抽象工廠)的客戶。
而抽象工廠是一個定義了如何產生一個關聯產品的家族。這個家族包含了所有製作手機的組件。
以台灣的組件工廠而言,具象的台灣工廠負責產生準備台灣手機的組件,如何產生符合自己區域的正確物件。
而右邊是對於產品家族,每個工廠都有不同的實踐方式!!
假如你觀察到了,你會問:工廠方法是不是潛伏在抽象工廠裡面呢?(prepareUSB、installOS…etc不都是宣告成抽象,而次類別來推翻實踐嗎?)
抽象工廠確實經常以工廠方法的方式實踐,這很有道理,抽象工廠的任務是定義一個介面,而這個介面是負責建立一個具象產品,
同時利用實踐抽象工廠的次類別,提供這些具體作法。所以在抽象工廠中,利用工廠方法實踐產品方法,是很自然的。
但你會發現工廠方法和抽象工廠都是負責建立物件,但是工廠方法是透過繼承的方式,而抽象工廠是透過物件合成。
所以這意味著工廠方法建立物件必須去繼承一個類別,然後推翻他的工廠方法。
整個工廠模式不外乎就是藉由繼承來的次類別建立物件。而客戶只需要知道抽象的類別就好,由次類別負責決定具象型態。
工廠模式只負責將客戶從具象型態中鬆綁。
抽象工廠也做的到這一點,只是做法不同:
抽象工廠提供了一個抽象型態,用來建立一個產品家族。這個型態的次類別定義了產品被產生的方法。欲使用這個工廠,必須先實體化一個工廠
然後傳進一些針對抽象型態寫的程式。而家族的好處即是將一群相關的產品集結起來。
不過抽象工廠的問題是,如果家族加入了新產品,就必須改變介面…從規模而言。
工廠方法只需要一個方法,僅是建立一個產品。但抽象工廠需要一個大的介面,因為他要被用來建立整個產品家族。
所以每當需要建立產品家族以及讓製造的相關產品集結起來時,請選用抽象工廠。
而當你想要單純將客戶程式碼從需要被實體化的具象類別中鬆綁。或著是目前不知道將來需要實體化哪些具象類別的時候
就可以使用工廠方法。只要繼承工廠方法成為次類別,並實踐就好了。
知道使用時機了嗎?
後記:深入淺出設計模式,這本書從工廠模式定義了這兩個:工廠方法與抽象工廠模式,這兩個模式都是將物件建立的過程封裝起來,以便鬆綁程式碼與具象的類別
過完了這一章,也學到了設計模式的一個里程碑之一,工廠方法非常常被用到,像是啊.NET Framework中的物件建立很多都是透過工廠方法。
而像Flash中很多UI的物件定義也都是透過工廠模式來定義。所以在學習架構的設計上,工廠方法應該要致力去學會,你將會非常非常常看到它。
下一章預告獨體模式(Singleton DesingPattern)。