在上一講我們談到了利用工廠方法模式解決對"某個對象"的創建工作,通常這個對象是"易變的",但它們的抽象能力卻相對固定(即主模塊變化相對緩慢),因此我們將通過工廠的"創建"來"封裝"這個變化點,使得客戶端無需知道對象的具體類型。但不管是簡單工廠也好,工廠方法也罷都是針對"一類"對象的創建,當面對"一系列"(我們這里可以稱維度)相互依賴的的對象時,就已經力不從心了,可能工廠的數量會指數級增長,這個可能是我們目前面臨的變化點,我們同樣需要一種"封裝機制"來隔離這個變化點,那就是所謂的抽象工廠模式。我們都知道創建型模式都是解決new的問題,即讓new的對象不依賴於某個具體的類型,在講解抽象工廠模式之前,有兩點需要強調(並且我將一直強調,因為這可能是是否采用設計模式的標准):1.封裝變化點:即哪里變化,封裝哪里。2.相反,如果沒有變化,當然不需要額外的封裝。
可能一個例子會來的更實在些:假設我們有一天需要配置一台電腦,首先我們應該去電腦城並從工作人員處索取各種電腦需要的配置信息,可能包括CPU、內存、硬盤以及其他信息,我們以CPU和內存為例,可能工作人員會給我們提供Intel或AMD的CPU,並告知我們對應的插孔以及接口什么的,我們會從這些信息中總結出幾套組裝信息,然后選擇其中一套給配貨員來配置一台電腦,但我們知道有些設備之間存在兼容性,所以只能選擇兼容性的一直的設備組裝。例子講到這,可能大家會問這跟我們的抽象工廠有何關系?首先我們可以把自己當成客戶端,配貨員當成是工廠,CPU、內存以及其他設備當成是產品對象,而電腦是由這些產品對象組合起來的產品簇。剛才也講了,由多個產品對象經過多種組合可以構建不同的電腦構成了"多系列",而這些產品對象時相互依賴的,配貨員只需要知道創建裝機方案(CPU、內存等組合,可能看成了工廠集合),便可以輕而易舉的組裝一台電腦。有了這個感性認識后,我們來看抽象工廠的設計意圖。
設計意圖:提供一個接口,讓該接口負責創建"一系列相互依賴的對象",而無需知道具體的實現類。
應用場景:在軟件系統中,經常面臨"一系列相互依賴的對象"的創建工作,同時由於需求的變化,往往存在更多系列的創建工作。如何應對這種變化?如何繞過常規的對象創建方法new,提供一種"封裝機制"來避免客戶端和"多系列具體對象"的緊耦合。
實際案例:我們經常在生活中出行一般靠公交車和地鐵,當我們到達固定的乘車地點(可能是公交車站或地鐵站),開始買票,一般都會根據你上車的車站算起(個別城市除外,以按里程計價為准)。那對我們來說,要從售票系統購買一張車票(產品簇),我們需要兩個購買接口(車票和車站),同時對於公交車而言只能使用公交車票,地鐵只能使用地鐵票,兩者不能通用。將這個場景在一張圖上表示出來:

我們從圖中可以看出客戶端僅僅與抽象對象有關(工廠、產品),而與具體的實現類無關。我們只要選擇相應的交通工具和始發車站,就能購買一張對應的車票。而構成車票的交通工具和始發車站都是彼此依賴的,但要注意的是:多系列之間是不依賴的,如公交和地鐵是彼此不依賴的。 看了這張圖之后,我們接下來用程序來實現。
定義車票
public abstract class Ticket { public abstract string TicketNo { get; } public abstract decimal BasePrice { get; } } public class BusTicket : Ticket { public override string TicketNo { get { return "BT-001"; } } public override decimal BasePrice { get { return 1m; } } } public class SubwayTicket : Ticket { public override string TicketNo { get { return "ST-001"; } } public override decimal BasePrice { get { return 2m; } } }
定義車站
public abstract class Station { public abstract string Name { get; } } public class BusStation : Station { public override string Name { get { return "高升橋"; } } } public class SubwayStation : Station { public override string Name { get { return "紅牌樓"; } } }
定義抽象工廠
public interface IFactory { Ticket CreateTicket(); Station CreateStation(); } public class BusFactory : IFactory { public Ticket CreateTicket() { return new BusTicket(); } public Station CreateStation() { return new BusStation(); } } public class SubwayFactory : IFactory { public Ticket CreateTicket() { return new SubwayTicket(); } public Station CreateStation() { return new SubwayStation(); } }
建立客戶端測試
[TestMethod] public void TestBusTicketSystem() { //創建公交車工廠 var factory = GetFactoryByConfig("BusFormat"); var ticket = factory.CreateTicket(); Assert.AreEqual<string>("BT-001", ticket.TicketNo); Assert.AreEqual<decimal>(1m, ticket.BasePrice); var station = factory.CreateStation(); Assert.AreEqual<string>("高升橋", station.Name); } [TestMethod] public void TestSubwayTicketSystem() { //創建地鐵工廠 var factory = GetFactoryByConfig("SubwayFormat"); var ticket = factory.CreateTicket(); Assert.AreEqual<string>("ST-001", ticket.TicketNo); Assert.AreEqual<decimal>(2m, ticket.BasePrice); var station = factory.CreateStation(); Assert.AreEqual<string>("紅牌樓", station.Name); } private IFactory GetFactoryByConfig(string formatKey) { var format = ConfigurationManager.AppSettings[formatKey]; var appPath = AppDomain.CurrentDomain.BaseDirectory; var assembly = Assembly.LoadFrom(Path.Combine(appPath, "DesignPattern.AbstractFactoryPattern.dll")); var type = "DesignPattern.AbstractFactoryPattern." + format + "Factory"; var factory = (IFactory)assembly.CreateInstance(type); return factory; }
從以上實例我們可以看出: 如果不存在"多系列對象創建"的需求變化,則不宜采用抽象工廠,而采用簡單的靜態工廠即可。假設目前只有公交車這種交通出行方式,那么在這里的抽象工廠也就沒有多大意義可言。其次,抽象工廠的創建任務集合中的對象時相互依賴的,如車票和車站是有對應關系的,而不同系列(如公交和地鐵)是不存在依賴關系的。抽象工廠主要應對"新系列"的需求變動,如這套售票系統將來可能推廣到出租車系統等,只需要添加一個出租車工廠和對應的出租車票和出租車站即可。但卻不能應對"新對象"的需求變動,如現在用戶除了選擇車票類型和始發車站之外,還需要選擇車型,車次等。這樣就會去改動抽象工廠的構建方式(主模塊發生了較大變動),就使得"多系列對象創建"跟客戶端緊耦合了。如果要應對這種"新對象"的需求變動,可以結合抽象工廠和工廠方法模式來解決。
從以上實例我們繼續探討,可能對於用戶來說(客戶端),更關心的是直接能拿到票,只需要選擇是公交車還是地鐵即可(也就是只需要知道采用的工廠創建方案),那樣就更容易和簡化購票流程(至於內部到底起價多少,當時乘車的站名等等信息不太關心)。對我們而言,只需要給用戶提供一種選擇方案,也許才算是最好的報答。那接下來看看程序如何實現。
/// <summary> /// 定義售票窗口(客戶端) /// </summary> public class Client { //定義訂票需要的項 private Ticket ticket = null; private Station station = null; //售票窗口接口 public void BuyTicket(IFactory factory) { Prepare(factory); } //訂票的具體准備工作 private void Prepare(IFactory factory) { //使用工廠獲取對應的子項信息 this.ticket = factory.CreateTicket(); this.station = factory.CreateStation(); } public Ticket Ticket { get { return this.ticket; } } public Station Station { get { return this.station; } } }
建立客戶端測試
[TestMethod] public void TestTicketSystem() { //獲取售票窗口(客戶端由用戶操作) var client = new Client(); //獲取售票工廠(公交車) var factory = GetFactoryByConfig("BusFormat"); //已經購買一張公交車票 client.BuyTicket(factory); //驗證是否正確 Assert.IsNotNull(client.Ticket); Assert.AreEqual<string>("BT-001", client.Ticket.TicketNo); Assert.AreEqual<decimal>(1m, client.Ticket.BasePrice); Assert.AreEqual<string>("高升橋", client.Station.Name); }
從以上的代碼我們發現客戶端僅僅與抽象工廠和抽象對象有關,而與具體的實現無關,這也就達到了我們常說的"面向接口編程,而與具體實現無關"。秉承"工廠保持簡潔"原則,我們不難發現BusFactory、SubwayFactory的創建工作是如此的類似,那是否可以說把他們統一起來呢?我們這里采用抽象類與反射來封裝這個細節。
public abstract class VehicleFactory { public static Ticket CreateTicket() { var type = ConfigurationManager.AppSetting["vehicle"]; return (Ticket)Activator.CreateInstance(Type.GetType(type)); } public static Station CreateStation() { var type = ConfigurationManager.AppSetting["vehicle"]; return (Station)Activator.CreateInstance(Type.GetType(type)); } }
