上一篇的工廠方法模式引入了工廠等級結構,解決了在原來簡單工廠模式中工廠類職責太重的原則,但是由於工廠方法模式的每個工廠只生產一類產品,可能會導致系統中存在大量的工廠類,從而增加系統開銷。那麼,我們應該怎麼來重構?似乎,我們可以考慮將一些相關的產品組成一個“產品族”,由同一個工廠來統一生產,這就是本次將要學習的抽象工廠模式的基本思想。
抽象工廠模式(Abstract Factory) | 學習難度:★★★★☆ | 使用頻率:★★★★★ |
一、介面皮膚庫的初始設計
M公司IT開發部接到一個開發任務,想要對以前的一個系統開發一套介面皮膚庫,可以對該桌面系統軟體進行介面美化。這樣,使用者就可以在使用時通過選單來選擇皮膚,不同的皮膚將提供視覺效果不同的按鈕、文字框以及組合框等介面元素,其結構示意圖如下所示:
該皮膚庫需要具備良好的靈活性和可擴充套件性,使用者可以自由選擇不同的皮膚,開發人員也可以在不修改既有程式碼的基礎上增加新的皮膚。
M公司的開發人員針對上述需求,決定現學現賣,在上次使用了工廠方法模式之後對工廠方法模式大為讚賞,決定使用工廠方法模式來進行系統的設計,為了保證系統的靈活性和可擴充套件性,提供一系列具體工廠來建立按鈕、文字框以及組合框等介面元素,客戶端針對抽象工廠來程式設計,初始結構如下圖所示:
從上圖可以看出,此方案提供了大量的工廠來建立具體的介面元件,可以通過配置檔案來更換具體介面元件從而改變介面的風格,但是,此方案存在以下問題:
(1)需要增加新的皮膚時,雖然不需要更改現有程式碼,但是需要增加大量的類,針對每一個新增具體元件都要增加一個具體工廠,類的個數會成對增加。這無疑會導致系統越來越龐大,從而增加了系統的維護成本和執行開銷。
(2)由於同一種風格的不同介面元件通常需要一起顯示,因此需要為每個元件都選擇一個具體工廠,使用者在使用時必須逐個進行設定,如果某個具體工廠選擇失誤將會導致介面顯示混亂,雖然可以適當增加一些約束語句,但是客戶端程式碼和配置檔案都較為複雜。
綜上所述,如何減少系統中類的個數並保證客戶端每次始終就只使用一種風格的具體介面元件?這是縈繞在M公司開發人員心頭的兩個問題。
二、抽象工廠模式介紹
2.1 理清產品等級與產品族
(1)產品等級
產品等級即產品的繼承結構,例如一個抽象類是電視機,其子類有海爾電視機、海信電視機、TCL電視機,則抽象電視機與具體品牌的電視機之間構成了一個產品等級結構,抽象電視機是父類,而具體品牌電視機則是子類。
(2)產品族
產品族是指由同一個工廠生產的,位於不同產品等級結構中的一組,例如海爾電器廠生產的海爾電視機、海爾電冰箱,海爾電視機位於電視機產品等級結構中,而海爾電冰箱則位於電冰箱產品等級結構中,他們倆構成了一個產品族。
產品等級與產品族的示意圖如下圖所示:
可以看出,當系統所提供的工廠生產的具體產品並不是一個簡單的物件,而是多個位於不同產品等級結構、屬於不同型別的具體產品時,就可以使用抽象工廠模式。抽象工廠模式是所有形式的工廠模式中最為抽象和最具一般性的一種形式。
Note :抽象工廠與工廠方法最大的區別就在於,工廠方法模式針對的是一個產品等級結構,而抽象工廠模式需要面對多個產品等級結構,一個工廠等級結構可以負責多個不同產品等級中的產品物件的建立。
2.2 抽象工廠模式概述
抽象工廠模式為建立一組物件提供了一種方案,與工廠方法模式相比,抽象工廠模式中的具體工廠不只是建立一種產品,它負責建立一族產品。其定義如下:
Definition :抽象工廠模式提供一個建立一系列相關或相互依賴物件的介面,而無須指定它們具體的類。抽象工廠模式又稱為Kit模式,它是一種物件建立型模式。
抽象工廠模式的結構圖如下圖所示:
(1)Abstract Factory (抽象工廠角色):宣告瞭一組用於建立一族產品的方法,每一個方法對應一種產品。
(2)Concrete Factory (具體工廠角色):實現了在抽象工廠中宣告的建立產品的方法,生成一組具體產品,這些產品構成了一個產品族。
(3)Abstract Product (抽象產品角色):為每種產品宣告介面,在抽象產品中宣告瞭所有的業務方法。
(4)Concrete Product (具體產品角色):定義具體工廠生產的具體產品物件,實現在抽象產品介面中宣告的業務方法。
三、介面皮膚庫的重構設計
3.1 抽象工廠模式設計方案
M公司使用抽象工廠模式來重構了介面皮膚庫的設計,其基本結構如下圖所示:
3.2 重構程式碼
(1)Abstract Product
public interface IButton { void Display(); } public interface ITextField { void Display(); } public interface IComboBox { void Display(); }
(2)Concrete Product
① Spring風格Button
public class SpringButton : IButton { public void Display() { Console.WriteLine("顯示淺綠色按鈕..."); } } public class SpringTextField : ITextField { public void Display() { Console.WriteLine("顯示綠色邊框文字框..."); } } public class SpringComboBox : IComboBox { public void Display() { Console.WriteLine("顯示綠色邊框下拉框..."); } }
② Summer風格Button
public class SummerButton : IButton { public void Display() { Console.WriteLine("顯示淺藍色按鈕..."); } } public class SummerTextField : ITextField { public void Display() { Console.WriteLine("顯示藍色邊框文字框..."); } } public class SummerComboBox : IComboBox { public void Display() { Console.WriteLine("顯示藍色邊框下拉框..."); } }
(3)Abstract Factory
public interface ISkinFactory { IButton CreateButton(); ITextField CreateTextField(); IComboBox CreateComboBox(); }
(4)Concrete Factory
① Spring皮膚工廠
// Spring皮膚工廠 public class SpringSkinFactory : ISkinFactory { public IButton CreateButton() { return new SpringButton(); } public IComboBox CreateComboBox() { return new SpringComboBox(); } public ITextField CreateTextField() { return new SpringTextField(); } }
② Summer皮膚工廠
public class SummerSkinFactory : ISkinFactory { public IButton CreateButton() { return new SummerButton(); } public IComboBox CreateComboBox() { return new SummerComboBox(); } public ITextField CreateTextField() { return new SummerTextField(); } }
3.3 測試結果
(1)客戶端程式碼
public class Program { public static void Main(string[] args) { ISkinFactory skinFactory = (ISkinFactory) AppConfigHelper.GetSkinFactoryInstance(); if (skinFactory == null) { Console.WriteLine("讀取當前選中皮膚型別失敗..."); } IButton button = skinFactory.CreateButton(); ITextField textField = skinFactory.CreateTextField(); IComboBox comboBox = skinFactory.CreateComboBox(); button.Display(); textField.Display(); comboBox.Display(); Console.ReadKey(); } }
其中,AppConfigHelper用於從以下的配置檔案中讀取SkinFactory的值去反射生成具體工廠例項,這裡配置的當前選中皮膚是Spring風格。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <!-- 當前選中皮膚型別 --> <add key="SkinFactory" value="Manulife.ChengDu.DesignPattern.AbstractFactory.SpringSkinFactory, Manulife.ChengDu.DesignPattern.AbstractFactory"/> </appSettings> </configuration>
AppConfigHelper的具體程式碼如下:
public class AppConfigHelper { public static string GetSkinFactoryName() { string factoryName = null; try { factoryName = System.Configuration.ConfigurationManager.AppSettings["SkinFactory"]; } catch (Exception ex) { Console.WriteLine(ex.Message); } return factoryName; } public static object GetSkinFactoryInstance() { string assemblyName = AppConfigHelper.GetSkinFactoryName(); Type type = Type.GetType(assemblyName); var instance = Activator.CreateInstance(type); return instance; } }
(2)除錯結果
終於到了興奮的時刻啦,執行結果如下:
這時,我們將配置檔案中的值改為SummerSkinFactory試試:
<add key="SkinFactory" value="Manulife.ChengDu.DesignPattern.AbstractFactory.SummerSkinFactory, Manulife.ChengDu.DesignPattern.AbstractFactory"/>
再次執行程式,結果是我們想要的:
四、抽象工廠模式總結
4.1 抽象工廠模式主要優點
(1)隔離了具體類的生成,使得客戶並不需要知道什麼被建立。因為這種隔離,因此更換一個具體工廠就變得相對容易。
(2)當一個產品族中的多個物件被設計稱一起工作時,它能夠保證客戶端始終只使用同一個產品族中的物件。
(3)增加新的產品族很方便,無需修改已有系統,符合開閉原則。
4.2 抽象工廠模式主要缺點
增加新的產品等級結構很麻煩,增加新的產品等級結構很麻煩,增加新的產品等級結構很麻煩!!!(重要的事情說三遍)因為需要對原有系統進行較大的修改,甚至需要修改抽象層程式碼,這必然會帶來較大的不便,在這個角度,它違背了開閉(對擴充套件開放,對修改封閉)原則。
想想,如果我們需要為單選按鈕(RadioButton)提供不同皮膚的風格化顯示,會發現無論選擇哪種皮膚,單選按鈕都顯得“格格不入”。
4.3 抽象工廠模式應用場景
(1)使用者無須關心物件的建立過程,需要將物件的建立和使用解耦 -> 這是所有工廠模式的使用前提
(2)系統中有多餘一個的產品族,而每次都只使用其中的某一種產品族。 -> 可以通過配置檔案等方式來使得使用者可以動態地改變產品族,也可以很方便地增加新的產品族
(3)產品等級結構穩定!設計完成之後,不會向系統中增加新的產品等級結構或刪除已有產品等級結構。 -> 並不太符合開閉原則
參考資料
劉偉,《設計模式的藝術—軟體開發人員內功修煉之道》