【設計模式】工廠方法模式 Factory Method Pattern


簡單工廠模式中產品的創建統一在工廠類的靜態工廠方法中創建,體現了面形對象的封裝性,客戶程序不需要知道產品產生的細節,也體現了面向對象的單一職責原則(SRP),這樣在產品很少的情況下使用起來還是很方便, 但是如果產品很多,並且不斷的有新產品加入,那么就會導致靜態工廠方法變得極不穩定,每次加入一個新產品就要修改靜態工廠方法,這違背了面向對象設計原則的開閉原則(OCP)。那么在應對這種不斷增加的新產品,簡單工模式有些力不從心了,那么什么模式可以完美應對呢?這就是這篇文章要談到的工廠方法模式。在工廠方法模式中,我們不再提供一個統一的工廠類來創建所有的產品對象,而是針對不同的產品提供不同的工廠類,系統提供一個與產品等級結構對應的工廠等級結構。

一、工廠方法模式定義

工廠方法模式(Factory Method Pattern):定義一個用於創建對象的接口,讓子類決定將哪一個類實例化。工廠方法模式讓一個類的實例化延遲到其子類。工廠方法模式又簡稱為工廠模式(Factory Pattern),又可稱作虛擬構造器模式(Virtual Constructor Pattern)或多態工廠模式(Polymorphic Factory Pattern)。

二、工廠方法模式結構圖

image

工廠方法模式結構圖

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();
}

輸出結果:

image

 

四、重構音頻播放器實例得到工廠方法模式

簡單工廠模式中我們舉了一個音頻播放器的例子,開發人員從開始直接創建對象中逐步隨着需求的改變最終得到了簡單工廠模式, 完美的解決了播放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();
    }
}

輸出結果:

image

看起來很不錯,完美的解決了播放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();
    }
}

運行軟件輸出結果:

image

代碼重構完成,結構符合預期,在回過頭來Review 一下代碼,這不就是Factory Method Pattern嗎? 這樣開發人員就將這種場景下的代碼構造的比較合理了。甲方再增加新的音頻文件格式時,就很容易應對了,只需要創建一個具體產品並且再創建一個具體的工廠類來創建這個產品就可以了。這樣軟件更符合面向對象設計原則的SRPOCP原則了。

下來問題來了, 如果甲方提出需要這個播放器軟件支持視頻播放,開發人員應該怎么辦能? 那么 隨着學習其他模式就能找到更合理的答案。

五、工廠方法模式的優點:

  1. 在工廠方法模式中,工廠方法用來創建客戶所需要的產品,同時還向客戶隱藏了哪種具體產品類將被實例化這一細節,用戶只需要關心所需產品對應的工廠,無須關心創建細節,甚至無須知道具體產品類的類名。
  2. 基於工廠角色和產品角色的多態性設計是工廠方法模式的關鍵。它能夠讓工廠可以自主確定創建何種產品對象,而如何創建這個對象的細節則完全封裝在具體工廠內部。工廠方法模式之所以又被稱為多態工廠模式,就正是因為所有的具體工廠類都具有同一抽象父類。
  3. 使用工廠方法模式的另一個優點是在系統中加入新產品時,無須修改抽象工廠和抽象產品提供的接口,無須修改客戶端,也無須修改其他的具體工廠和具體產品,而只要添加一個具體工廠和具體產品就可以了,這樣,系統的可擴展性和靈活性也就變得非常好,維護起來就變得簡單了,完全符合“開閉原則(OCP)”。

六、工廠方法模式的缺點:

  1. 在添加新產品時,需要編寫新的具體產品類,而且還要提供與之對應的具體工廠類,系統中類的個數將成對增加,在一定程度上增加了系統的復雜度,有更多的類需要編譯和運行,會給系統帶來一些額外的開銷。
  2. 由於考慮到系統的可擴展性,需要引入抽象層,在客戶端代碼中均使用抽象層進行定義,增加了系統的抽象性和理解難度,且在實現時可能需要用到反射等技術,增加了系統的實現難度。

七、工廠方法模式的使用場景:

  1. 客戶端不知道它所需要的對象的類。在工廠方法模式中,客戶端不需要知道具體產品類的類名,只需要知道所對應的工廠即可,具體的產品對象由具體工廠類創建,可將具體工廠類的類名存儲在配置文件或數據庫中。
  2. 抽象工廠類通過其子類來指定創建哪個對象。在工廠方法模式中,對於抽象工廠類只需要提供一個創建產品的接口,而由其子類來確定具體要創建的對象,利用面向對象的多態性和里氏代換原則,在程序運行時,子類對象將覆蓋父類對象,從而使得系統更容易擴展。有了這么一個特點, 我們可以在軟件的運行時改變系統的功能,進而實現熱插拔。 

八、擴展-使用配置+反射動態創建特定工廠實現工廠熱替換

以上面的音樂播放器為例, 如果在特定的場景下只需要播放MP3  格式的音樂,而在另一些特定的場景下只需要播放ACC    格式的音樂, 怎么辦呢?

這里使用配置+反射動態創建 工廠的方式來實現這個需求, 首先來看怎么實現僅支持MP3的情況:

1. 在配置文件App.config中增加一個配置:

<appSettings>
	<add key="MethodFactory" value="DesignPattern.MehodFactory.AudioInstance.Mp3Factory"/>
</appSettings>
 

2.在調用處讀取上面的配置文件並使用反射得到具體工廠,然后調用Play  方法,代碼如下:

static void AudioMethodFactoryExecuteBySetting()
{
    DesignPattern.MehodFactory.AudioInstance.IFactory factory=null;
    var setting = ConfigurationSettings.AppSettings["MethodFactory"];
    string dir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
    string[] assemblies = Directory.GetFiles(dir, "*.exe");
    List<Type> types = new List<Type>();
    foreach (var s in assemblies)
    {
        var ass=Assembly.LoadFile(s);
        foreach(Type t in ass.GetExportedTypes()){
            if(t.IsClass && typeof( DesignPattern.MehodFactory.AudioInstance.IFactory).IsAssignableFrom(t)){
                if(t.FullName==setting){
                    factory=Activator.CreateInstance(t) as DesignPattern.MehodFactory.AudioInstance.IFactory;                   
                }
            }
        }
    }   

    if (factory == null) return;

    IAudio audio = factory.Create();
    audio.Play("take me to your hert");
}

輸出結果:

image

如果需求改為僅支持ACC格式的音頻文件,就很容易實現了,僅僅需要修改配置文件中配置的具體工廠類的字符串就可以了,其它任何地方都不需要改變,並且不需要編譯應用程序就可以正常工作了。

這里我們找到應用程序生成的目錄:F:\source\DesignPattern\DesignPattern.MehodFactory\bin\Debug,在這里我們看到有下列文件:

image

我們只需要用寫字板打開配置文件 DesignPattern.MehodFactory.exe.config  修改配置為需要支持的ACCFactory就可以了

image

然后雙擊文件DesignPattern.MehodFactory.exe運行,結果如下:

image

我們看到僅僅只改變了一下配置就輕松實現了應用功能的熱替換,不需要做任何的編譯和代碼上的修改。

九.無源碼擴展

假如這個應用程序是從其它的軟件開發商那里買來的,現在你的老板然你開發一個新的功能,需要在某些場景下僅支持AAR格式的音頻文件,該怎么辦呢?。

1.新建一個控制台應用程序

假設現在沒有源代碼,但是還要實現支持AAR的音頻格式文件的播放, 首先需要重新建一個C# 的工程文件,創建一個控制台應用程序,這里我命名為DesignPattern.Extension, 然后創建一個Aar class 並繼承IAudio接口,再創建一個AarFactory class並集成IFactory接口, 代碼如下:

public class AarFactory : DesignPattern.MehodFactory.AudioInstance.IFactory
{
    public IAudio Create()
    {
        return new Aar();
    }
}

public class Aar : IAudio
{
    public void Play(string name)
    {
        Console.WriteLine("Start playing Aar...");
        Console.WriteLine(string.Format("The song name is: [{0}.aar]", name));
        Console.WriteLine("..........");
    }
}

寫完代碼后編譯DesignPattern.Extension 應用程序然后找到生成的DesignPattern.Extension.exe 文件, 然后拷貝到F:\source\DesignPattern\DesignPattern.MehodFactory\bin\Debug, 如下:

image

2. 修改配置文件如下:

image

3. 雙擊DesignPattern.MehodFactory.exe 運行, 看到下面的結果輸出:

image

我們看到輸出的正是Arr文件的邏輯。

這樣就輕松實現了無源碼的擴展。

 

注意:這里我使用的是控制台應用程序,其擴展名是.exe, 所以在反射的時候我掃碼的是當前工作目錄下的所有exe后綴的文件,如果是類庫工程,就要掃描當前工作目錄下的dll文件。並且還要將exe文件也掃描進去,不然當前程序集中實現的工廠無法找到。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM