在簡單工廠模式中產品的創建統一在工廠類的靜態工廠方法中創建,體現了面形對象的封裝性,客戶程式不需要知道產品產生的細節,也體現了面向對象的單一職責原則(SRP),這樣在產品很少的情況下使用起來還是很方便, 但是如果產品很多,並且不斷的有新產品加入,那麼就會導致靜態工廠方法變得極不穩定,每次加入一個新產 ...
在簡單工廠模式中產品的創建統一在工廠類的靜態工廠方法中創建,體現了面形對象的封裝性,客戶程式不需要知道產品產生的細節,也體現了面向對象的單一職責原則(SRP),這樣在產品很少的情況下使用起來還是很方便, 但是如果產品很多,並且不斷的有新產品加入,那麼就會導致靜態工廠方法變得極不穩定,每次加入一個新產品就要修改靜態工廠方法,這違背了面向對象設計原則的開閉原則(OCP)。那麼在應對這種不斷增加的新產品,簡單工模式有些力不從心了,那麼什麼模式可以完美應對呢?這就是這篇文章要談到的工廠方法模式。在工廠方法模式中,我們不再提供一個統一的工廠類來創建所有的產品對象,而是針對不同的產品提供不同的工廠類,系統提供一個與產品等級結構對應的工廠等級結構。
一、工廠方法模式定義
工廠方法模式(Factory Method Pattern):定義一個用於創建對象的介面,讓子類決定將哪一個類實例化。工廠方法模式讓一個類的實例化延遲到其子類。工廠方法模式又簡稱為工廠模式(Factory Pattern),又可稱作虛擬構造器模式(Virtual Constructor Pattern)或多態工廠模式(Polymorphic Factory Pattern)。
二、工廠方法模式結構圖
工廠方法模式結構圖
1.IProduct (抽象產品角色):
它是定義產品的介面,是工廠方法模式所創建對象的父類,也就是產品對象的公共父類,這個角色一般可以有抽象類或者介面來擔當。
2.ConcreteProduct(具體產品):
它實現了抽象產品介面,某種類型的具體產品由專門的具體工廠創建,具體工廠和具體產品之間一一對應。
3.Factory(抽象工廠):
在抽象工廠類中,聲明瞭工廠方法(Factory Method),用於返回一個產品。抽象工廠是工廠方法模式的核心,所有創建具體對象的具體工廠類都必須實現該介面。
4. ConcreteFactory(具體工廠):
它是抽象工廠類的子類,實現了抽象工廠中定義的工廠方法,並可由客戶端調用,返回一個具體產品類的實例。
與簡單工廠模式相比,工廠方法模式最重要的區別是引入了抽象工廠角色,抽象工廠可以是介面,也可以是抽象類或者具體類
三、工廠發發模式代碼實現:
public interface IProduct { void DoSomething(); } public interface IFactory { IProduct Create(); } public class ConcreteProductA : IProduct { public void DoSomething() { Console.WriteLine("I'm Product A"); } } public class ConcreteProductB : IProduct { public void DoSomething() { Console.WriteLine("I'm Product B"); } } public class ConcreteFactoryA : IFactory { public IProduct Create() { return new ConcreteProductA(); } } public class ConcreteFactoryB : IFactory { public IProduct Create() { return new ConcreteProductB(); } }
客戶端調用:
static void Main() { //使用ConcreteFactoryA 創建 ProductA IFactory factoryA = new ConcreteFactoryA(); IProduct productA = factoryA.Create(); productA.DoSomething(); //使用ConcreteFactoryB 創建 ProductB IFactory factoryB = new ConcreteFactoryB(); IProduct productB = factoryB.Create(); productB.DoSomething(); Console.ReadKey(); }
輸出結果:
四、重構音頻播放器實例得到工廠方法模式
在簡單工廠模式中我們舉了一個音頻播放器的例子,開發人員從開始直接創建對象中逐步隨著需求的改變最終得到了簡單工廠模式, 完美的解決了播放MP3,WAV,WMA格式的音頻文件。最終代碼看起來是這樣:
public interface IAudio { void Play(string name); } public class Wma : IAudio { public void Play(string name) { Console.WriteLine("Start playing wma file..."); Console.WriteLine($"The song name is: [{name}.wma]"); Console.WriteLine(".........."); } } public class Wav : IAudio { public void Play(string name) { Console.WriteLine("Start playing wav file..."); Console.WriteLine($"The song name is: [{name}.wav]"); Console.WriteLine(".........."); } } public class Mp3 : IAudio { public void Play(string name) { Console.WriteLine("Start playing mp3..."); Console.WriteLine($"The song name is: [{name}.mp3]"); Console.WriteLine(".........."); } } public class AudioFactory { public static IAudio Create(string songType) { IAudio audio; switch (songType.ToUpper()) { case "A": audio = new Wav(); break; case "M": audio = new Wma(); break; case "P": audio = new Mp3(); break; default: throw new ArgumentException("Invalid argument", nameof(songType)); } return audio; } } [Description("1.2. Simple Factory")] public class App { static void Main() { Console.WriteLine("Please input a or m or p"); var input = Console.ReadKey(); if (input != null) { IAudio audio = AudioFactory.Create(input.Key.ToString()); audio.Play("take me to your hert"); } Console.ReadKey(); } }
輸出結果:
看起來很不錯,完美的解決了播放WMA,WAV和MP3 格式的音頻文件,但是音樂文件的格式不斷在發展增多,因此播放器也要通過不斷的升級來支持不斷涌現的新格式的音頻文件。 甲方已經提出來了支持MPEG, MPEG-4 等等格式的文件,每次開發人員都要新增一個具體的音頻格式的類,並且在工廠的靜態方法中創建一個case條件來支持新的格式文件。日積月累,隨著時間的推移,swich case 的邏輯變得異常的龐大和複雜,很難維護了,這不,最近甲方提出來要支持acc格式文件的播放,這次升級終於是產生了一次事故, 開發人員從甲方哪裡拿到要支持acc音頻格式的文件需求,輕車熟路創建了個acc的產品文件類,但是忘記在swich case 中加這個case就將代碼編譯打包提交給甲方。由於甲方和開發人員過去每次配合的都很好,這一次他就絕對的信任了開發人員,於是沒有測試新的版本就直接發佈到市場上投入了商業使用。結果可想而知根本就播放不了acc格式的音頻文件。 甲方知道此事後很生氣,勒令開發人員立馬修複bug重新發佈版本,但是市場是瞬息萬變的,就因為這麼一個失誤的發佈,市場上的竟品軟體就很快蠶食了甲方播放器的市場。開發人員不敢怠慢,加班加點,找出bug並修複重新打包交付甲方,甲方趕緊將新版本經過充分測試後投入到市場。
隨後開發人員準備找出容易出現這種錯誤原因,將這種犯錯的機會扼殺在搖籃。除了自身的粗心之外,他還想從代碼上找到一些原因。於是他Review了一下自己的代碼, 他發現工廠類中的靜態工廠方法的邏輯太複雜了,翻滾了好幾個屏幕,看了一個多小時才把這裡面的代碼理順看清楚了, 看完後發發現靜態工廠方法的職責隨著產品的增多在不斷的增多, 工廠方法的負擔太重了, 他決定重構這個地方的代碼,他期望將創建具體產品的職責單提取到獨的一個類中來完成,一個類負責一個具體產品的創建,於是他提出了個這個創建具體產品的抽象介面IFactory, 然後讓具體創建類都繼承自這個介面, 通過重構代碼,現在音頻播放器的代碼變成了這樣:
public interface IAudio { void Play(string name); } public interface IFactory { IAudio Create(); } public class Wma : IAudio { public void Play(string name) { Console.WriteLine("Start playing wma file..."); Console.WriteLine($"The song name is: [{name}.wma]"); Console.WriteLine(".........."); } } public class Wav : IAudio { public void Play(string name) { Console.WriteLine("Start playing wav file..."); Console.WriteLine($"The song name is: [{name}.wav]"); Console.WriteLine(".........."); } } public class Mp3 : IAudio { public void Play(string name) { Console.WriteLine("Start playing mp3..."); Console.WriteLine($"The song name is: [{name}.mp3]"); Console.WriteLine(".........."); } } public class Acc : IAudio { public void Play(string name) { Console.WriteLine("Start playing Acc..."); Console.WriteLine($"The song name is: [{name}.acc]"); Console.WriteLine(".........."); } } public class WmaFactory : IFactory { public IAudio Create() { return new Wma(); } } public class WavFactory : IFactory { public IAudio Create() { return new Wav(); } } public class Mp3Factory : IFactory { public IAudio Create() { return new Mp3(); } } public class AccFactory : IFactory { public IAudio Create() { return new Acc(); } } [Description("2.1. Factory Mothed payer")] public class App { static void Main() { //Wma play IFactory wmaFactory = new WmaFactory(); IAudio wamAudio = wmaFactory.Create(); wamAudio.Play("take me to your hert"); //Wav play IFactory wavFactory = new WavFactory(); IAudio wavAudio = wavFactory.Create(); wavAudio.Play("take me to your hert"); //Mp3 play IFactory mp3Factory = new Mp3Factory(); IAudio mp3Audio = mp3Factory.Create(); mp3Audio.Play("take me to your hert"); //Acc play IFactory accFactory = new AccFactory(); IAudio accAudio = accFactory.Create(); accAudio.Play("take me to your hert"); Console.ReadKey(); } }
運行軟體輸出結果:
代碼重構完成,結構符合預期,在回過頭來Review 一下代碼,這不就是Factory Method Pattern嗎? 這樣開發人員就將這種場景下的代碼構造的比較合理了。甲方再增加新的音頻文件格式時,就很容易應對了,只需要創建一個具體產品並且再創建一個具體的工廠類來創建這個產品就可以了。這樣軟體更符合面向對象設計原則的SRP 和OCP原則了。
下來問題來了, 如果甲方提出需要這個播放器軟體支持視頻播放,開發人員應該怎麼辦能? 那麼 隨著學習其他模式就能找到更合理的答案。
五、工廠方法模式的優點:
- 在工廠方法模式中,工廠方法用來創建客戶所需要的產品,同時還向客戶隱藏了哪種具體產品類將被實例化這一細節,用戶只需要關心所需產品對應的工廠,無須關心創建細節,甚至無須知道具體產品類的類名。
- 基於工廠角色和產品角色的多態性設計是工廠方法模式的關鍵。它能夠讓工廠可以自主確定創建何種產品對象,而如何創建這個對象的細節則完全封裝在具體工廠內部。工廠方法模式之所以又被稱為多態工廠模式,就正是因為所有的具體工廠類都具有同一抽象父類。
- 使用工廠方法模式的另一個優點是在系統中加入新產品時,無須修改抽象工廠和抽象產品提供的介面,無須修改客戶端,也無須修改其他的具體工廠和具體產品,而只要添加一個具體工廠和具體產品就可以了,這樣,系統的可擴展性和靈活性也就變得非常好,維護起來就變得簡單了,完全符合“開閉原則(OCP)”。
六、工廠方法模式的缺點:
- 在添加新產品時,需要編寫新的具體產品類,而且還要提供與之對應的具體工廠類,系統中類的個數將成對增加,在一定程度上增加了系統的複雜度,有更多的類需要編譯和運行,會給系統帶來一些額外的開銷。
- 由於考慮到系統的可擴展性,需要引入抽象層,在客戶端代碼中均使用抽象層進行定義,增加了系統的抽象性和理解難度,且在實現時可能需要用到反射等技術,增加了系統的實現難度。
七、工廠方法模式的使用場景:
- 客戶端不知道它所需要的對象的類。在工廠方法模式中,客戶端不需要知道具體產品類的類名,只需要知道所對應的工廠即可,具體的產品對象由具體工廠類創建,可將具體工廠類的類名存儲在配置文件或資料庫中。
- 抽象工廠類通過其子類來指定創建哪個對象。在工廠方法模式中,對於抽象工廠類只需要提供一個創建產品的介面,而由其子類來確定具體要創建的對象,利用面向對象的多態性和里氏代換原則,在程式運行時,子類對象將覆蓋父類對象,從而使得系統更容易擴展。有了這麼一個特點, 我們可以在軟體的運行時改變系統的功能,進而實現熱插拔。