.NET設計模式:抽象工廠模式(Abstract Factory)
概述
在軟件系統中,經常面臨著“一系列相互依賴的對象”的創建工作;同時由于需求的變化,往往存在著更多系列對象的創建工作。如何應對這種變化?如何繞過常規的對象的創建方法(new),提供一種“封裝機制”來避免客戶程序和這種“多系列具體對象創建工作”的緊耦合?這就是我們要說的抽象工廠模式。
意圖
提供一個創建一系列相關或相互依賴對象的接口,而無需指定它們具體的類。
模型圖
邏輯模型:
物理模型:
生活中的例子
抽象工廠的目的是要提供一個創建一系列相關或相互依賴對象的接口,而不需要指定它們具體的類。這種模式可以汽車制造廠所使用的金屬沖壓設備中找到。這種沖壓設備可以制造汽車車身部件。同樣的機械用于沖壓不同的車型的右邊車門、左邊車門、右前擋泥板、左前擋泥板和引擎罩等等。通過使用轉輪來改變沖壓盤,這個機械產生的具體類可以在三分鐘內改變。
抽象工廠之新解
虛擬案例
中國企業需要一項簡單的財務計算:每月月底,財務人員要計算員工的工資。
員工的工資 = (基本工資 + 獎金 - 個人所得稅)。這是一個放之四海皆準的運算法則。
為了簡化系統,我們假設員工基本工資總是4000美金。
中國企業獎金和個人所得稅的計算規則是:
獎金 = 基本工資(4000) * 10%
個人所得稅 = (基本工資 + 獎金) * 40%
我們現在要為此構建一個軟件系統(代號叫Softo),滿足中國企業的需求。
案例分析
獎金(Bonus)、個人所得稅(Tax)的計算是Softo系統的業務規則(Service)。
工資的計算(Calculator)則調用業務規則(Service)來計算員工的實際工資。
工資的計算作為業務規則的前端(或者客戶端Client)將提供給最終使用該系統的用戶(財務人員)使用。
針對中國企業為系統建模
根據上面的分析,為Softo系統建模如下:
則業務規則Service類的代碼如下:
using System; namespace ChineseSalary { /**//// /// 公用的常量 /// public class Constant { public static double BASE_SALARY = 4000; } }
using System; namespace ChineseSalary { /// 計算中國個人獎金 public class ChineseBonus { public double Calculate() { return Constant.BASE_SALARY * 0.1; } } }
客戶端的調用代碼:
using System; namespace ChineseSalary { /// 計算中國個人所得稅 public class ChineseTax { public double Calculate() { return (Constant.BASE_SALARY + (Constant.BASE_SALARY * 0.1)) * 0.4; } } }
運行程序,輸入的結果如下:Chinese Salary is:2640
針對美國企業為系統建模
為了拓展國際市場,我們要把該系統移植給美國公司使用。
美國企業的工資計算同樣是: 員工的工資 = 基本工資 + 獎金 - 個人所得稅。
但是他們的獎金和個人所得稅的計算規則不同于中國企業:
美國企業獎金和個人所得稅的計算規則是:
獎金 = 基本工資 * 15 %
個人所得稅 = (基本工資 * 5% + 獎金 * 25%)
根據前面為中國企業建模經驗,我們僅僅將ChineseTax、ChineseBonus修改為AmericanTax、AmericanBonus。 修改后的模型如下:
則業務規則Service類的代碼如下:
using System; namespace AmericanSalary { /**//// /// 公用的常量 /// public class Constant { public static double BASE_SALARY = 4000; } }
using System; namespace AmericanSalary { /**//// /// 計算美國個人獎金 /// public class AmericanBonus { public double Calculate() { return Constant.BASE_SALARY * 0.1; } } }
using System; namespace AmericanSalary { /**//// /// 計算美國個人所得稅 /// public class AmericanTax { public double Calculate() { return (Constant.BASE_SALARY + (Constant.BASE_SALARY * 0.1)) * 0.4; } } }
客戶端的調用代碼:
using System; namespace AmericanSalary { /**//// /// 客戶端程序調用 /// public class Calculator { public static void Main(string[] args) { AmericanBonus bonus = new AmericanBonus(); double bonusValue = bonus.Calculate(); AmericanTax tax = new AmericanTax(); double taxValue = tax.Calculate(); double salary = 4000 + bonusValue - taxValue; Console.WriteLine("American Salary is:" + salary); Console.ReadLine(); } } }
運行程序,輸入的結果如下:
American Salary is:2640
整合成通用系統
讓我們回顧一下該系統的發展歷程:
最初,我們只考慮將Softo系統運行于中國企業。但隨著MaxDO公司業務向海外拓展, MaxDO需要將該系統移植給美國使用。
移植時,MaxDO不得不拋棄中國企業的業務規則類ChineseTax和ChineseBonus, 然后為美國企業新建兩個業務規則類: AmericanTax,AmericanBonus。最后修改了業務規則調用Calculator類。
結果我們發現:每當Softo系統移植的時候,就拋棄原來的類。現在,如果中國聯想集團要購買該系統,我們不得不再次拋棄AmericanTax,AmericanBonus,修改回原來的業務規則。
一個可以立即想到的做法就是在系統中保留所有業務規則模型,即保留中國和美國企業工資運算規則。
通過保留中國企業和美國企業的業務規則模型,如果該系統在美國企業和中國企業之間切換時,我們僅僅需要修改Caculator類即可。
讓移植工作更簡單
前面系統的整合問題在于:當系統在客戶在美國和中國企業間切換時仍然需要修改Caculator代碼。
一個維護性良好的系統應該遵循“開閉原則”。即:封閉對原來代碼的修改,開放對原來代碼的擴展(如類的繼承,接口的實現)
我們發現不論是中國企業還是美國企業,他們的業務運規則都采用同樣的計算接口。 于是很自然地想到建立兩個業務接口類Tax,Bonus,然后讓AmericanTax、AmericanBonus和ChineseTax、ChineseBonus分別實現這兩個接口, 據此修正后的模型如下:
此時客戶端代碼如下:
using System; namespace InterfaceSalary { /**//// /// 客戶端程序調用 /// public class Calculator { public static void Main(string[] args) { Bonus bonus = new ChineseBonus(); double bonusValue = bonus.Calculate(); Tax tax = new ChineseTax(); double taxValue = tax.Calculate(); double salary = 4000 + bonusValue - taxValue; Console.WriteLine("Chinaese Salary is:" + salary); Console.ReadLine(); } } }
為業務規則增加工廠方法
然而,上面增加的接口幾乎沒有解決任何問題,因為當系統的客戶在美國和中國企業間切換時Caculator代碼仍然需要修改。
只不過修改少了兩處,但是仍然需要修改ChineseBonus,ChineseTax部分。致命的問題是:我們需要將這個移植工作轉包給一個叫Hippo的軟件公司。 由于版權問題,我們并未提供Softo系統的源碼給Hippo公司,因此Hippo公司根本無法修改Calculator,導致實際上移植工作無法進行。
為此,我們考慮增加一個工具類(命名為Factory),代碼如下:
using System; namespace FactorySalary { /**//// /// Factory類 /// public class Factory { public Tax CreateTax() { return new ChineseTax(); } public Bonus CreateBonus() { return new ChineseBonus(); } } }
修改后的客戶端代碼:
using System; namespace FactorySalary { /**//// /// 客戶端程序調用 /// public class Calculator { public static void Main(string[] args) { Bonus bonus = new Factory().CreateBonus(); double bonusValue = bonus.Calculate(); Tax tax = new Factory().CreateTax(); double taxValue = tax.Calculate(); double salary = 4000 + bonusValue - taxValue; Console.WriteLine("Chinaese Salary is:" + salary); Console.ReadLine(); } } }
不錯,我們解決了一個大問題,設想一下:當該系統從中國企業移植到美國企業時,我們現在需要做什么?
答案是: 對于Caculator類我們什么也不用做。我們需要做的是修改Factory類,修改結果如下:
using System; namespace FactorySalary { /**//// /// Factory類 /// public class Factory { public Tax CreateTax() { return new AmericanTax(); } public Bonus CreateBonus() { return new AmericanBonus(); } } }
為系統增加抽象工廠方法
很顯然,前面的解決方案帶來了一個副作用:就是系統不但增加了新的類Factory,而且當系統移植時,移植工作僅僅是轉移到Factory類上,工作量并沒有任何縮減,而且還是要修改系統的源碼。 從Factory類在系統移植時修改的內容我們可以看出: 實際上它是專屬于美國企業或者中國企業的。名稱上應該叫AmericanFactory,ChineseFactory更合適.
解決方案是增加一個抽象工廠類AbstractFactory,增加一個靜態方法,該方法根據一個配置文件(App.config或者Web.config) 一個項(比如factoryName)動態地判斷應該實例化哪個工廠類,這樣,我們就把移植工作轉移到了對配置文件的修改。修改后的模型和代碼:
抽象工廠類的代碼如下:
using System; using System.Reflection; namespace AbstractFactory { /**//// /// AbstractFactory類 /// public abstract class AbstractFactory { public static AbstractFactory GetInstance() { string factoryName = Constant.STR_FACTORYNAME.ToString(); AbstractFactory instance; if(factoryName == "ChineseFactory") instance = new ChineseFactory(); else if(factoryName == "AmericanFactory") instance = new AmericanFactory(); else instance = null; return instance; } public abstract Tax CreateTax(); public abstract Bonus CreateBonus(); } }
配置文件:
xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="factoryName" value="AmericanFactory">add> appSettings> configuration>
采用上面的解決方案,當系統在美國企業和中國企業之間切換時,我們需要做什么移植工作?
答案是: 我們僅僅需要修改配置文件,將factoryName的值改為American。
修改配置文件的工作很簡單,只要寫一篇幅配置文檔說明書提供給移植該系統的團隊(比如Hippo公司) 就可以方便地切換使該系統運行在美國或中國企業。
最后的修正(不是最終方案)
前面的解決方案幾乎很完美,但是還有一點瑕疵,瑕疵雖小,但可能是致命的。
考慮一下,現在日本NEC公司決定購買該系統,NEC公司的工資的運算規則遵守的是日本的法律。如果采用上面的系統構架,這個移植我們要做哪些工作呢?
1、增加新的業務規則類JapaneseTax,JapaneseBonus分別實現Tax和Bonus接口。
2、修改AbstractFactory的getInstance方法,增加else if(factoryName.equals("Japanese")){....
注意: 系統中增加業務規則類不是模式所能解決的,無論采用什么設計模式,JapaneseTax,JapaneseBonus總是少不了的。(即增加了新系列產品)
我們真正不能接受的是:我們仍然修要修改系統中原來的類(AbstractFactory)。前面提到過該系統的移植工作,我們可能轉包給一個叫Hippo的軟件公司。 為了維護版權,未將該系統的源碼提供給Hippo公司,那么Hippo公司根本無法修改AbstractFactory,所以系統移植其實無從談起,或者說系統移植總要開發人員親自參與。
解決方案是將抽象工廠類中的條件判斷語句,用.NET中發射機制代替,修改如下:
using System; using System.Reflection; namespace AbstractFactory { /**//// /// AbstractFactory類 /// public abstract class AbstractFactory { public static AbstractFactory GetInstance() { string factoryName = Constant.STR_FACTORYNAME.ToString(); AbstractFactory instance; if(factoryName != "") instance = (AbstractFactory)Assembly.Load(factoryName). CreateInstance(factoryName); else instance = null; return instance; } public abstract Tax CreateTax(); public abstract Bonus CreateBonus(); } }
這樣,在我們編寫的代碼中就不會出現Chinese,American,Japanese等這樣的字眼了。
小結
最后那幅圖是最終版的系統模型圖。我們發現作為客戶端角色的Calculator僅僅依賴抽象類, 它不必去理解中國和美國企業具體的業務規則如何實現,Calculator面對的僅僅是業務規則接口Tax和Bonus。
Softo系統的實際開發的分工可能是一個團隊專門做業務規則,另一個團隊專門做前端的業務規則組裝。 抽象工廠模式有助于這樣的團隊的分工: 兩個團隊通訊的約定是業務接口,由抽象工廠作為紐帶粘合業務規則和前段調用,大大降低了模塊間的耦合性,提高了團隊開發效率。
完完全全地理解抽象工廠模式的意義非常重大,可以說對它的理解是你對OOP理解上升到一個新的里程碑的重要標志。 學會了用抽象工廠模式編寫框架類,你將理解OOP的精華:面向接口編程.。
應對“新對象”
抽象工廠模式主要在于應對“新系列”的需求變化。其缺點在于難于應付“新對象”的需求變動。如果在開發中出現了新對象,該如何去解決呢?這個問題并沒有一個好的答案,下面我們看一下李建忠老師的回答:
“GOF《設計模式》中提出過一種解決方法,即給創建對象的操作增加參數,但這種做法并不能令人滿意。事實上,對于新系列加新對象,就我所知,目前還沒有完美的做法,只有一些演化的思路,這種變化實在是太劇烈了,因為系統對于新的對象是完全陌生的。”
實現要點
抽象工廠將產品對象的創建延遲到它的具體工廠的子類。
如果沒有應對“多系列對象創建”的需求變化,則沒有必要使用抽象工廠模式,這時候使用簡單的靜態工廠完全可以。
系列對象指的是這些對象之間有相互依賴、或作用的關系,例如游戲開發場景中的“道路”與“房屋”的依賴,“道路”與“地道”的依賴。
抽象工廠模式經常和工廠方法模式共同組合來應對“對象創建”的需求變化。
通常在運行時刻創建一個具體工廠類的實例,這一具體工廠的創建具有特定實現的產品對象,為創建不同的產品對象,客戶應使用不同的具體工廠。
把工廠作為單件,一個應用中一般每個產品系列只需一個具體工廠的實例,因此,工廠通常最好實現為一個單件模式。
創建產品,抽象工廠僅聲明一個創建產品的接口,真正創建產品是由具體產品類創建的,最通常的一個辦法是為每一個產品定義一個工廠方法,一個具體的工廠將為每個產品重定義該工廠方法以指定產品,雖然這樣的實現很簡單,但它確要求每個產品系列都要有一個新的具體工廠子類,即使這些產品系列的差別很小。
優點
分離了具體的類。抽象工廠模式幫助你控制一個應用創建的對象的類,因為一個工廠封裝創建產品對象的責任和過程。它將客戶和類的實現分離,客戶通過他們的抽象接口操縱實例,產品的類名也在具體工廠的實現中被分離,它們不出現在客戶代碼中。
它使得易于交換產品系列。一個具體工廠類在一個應用中僅出現一次——即在它初始化的時候。這使得改變一個應用的具體工廠變得很容易。它只需改變具體的工廠即可使用不同的產品配置,這是因為一個抽象工廠創建了一個完整的產品系列,所以整個產品系列會立刻改變。
它有利于產品的一致性。當一個系列的產品對象被設計成一起工作時,一個應用一次只能使用同一個系列中的對象,這一點很重要,而抽象工廠很容易實現這一點。
缺點
難以支持新種類的產品。難以擴展抽象工廠以生產新種類的產品。這是因為抽象工廠幾口確定了可以被創建的產品集合,支持新種類的產品就需要擴展該工廠接口,這將涉及抽象工廠類及其所有子類的改變。
適用性
在以下情況下應當考慮使用抽象工廠模式:
一個系統不應當依賴于產品類實例如何被創建、組合和表達的細節,這對于所有形態的工廠模式都是重要的。
這個系統有多于一個的產品族,而系統只消費其中某一產品族。
同屬于同一個產品族的產品是在一起使用的,這一約束必須在系統的設計中體現出來。
系統提供一個產品類的庫,所有的產品以同樣的接口出現,從而使客戶端不依賴于實現。
應用場景
支持多種觀感標準的用戶界面工具箱(Kit)。
游戲開發中的多風格系列場景,比如道路,房屋,管道等……
總結
總之,抽象工廠模式提供了一個創建一系列相關或相互依賴對象的接口,運用抽象工廠模式的關鍵點在于應對“多系列對象創建”的需求變化。一句話,學會了抽象工廠模式,你將理解OOP的精華:面向接口編程。