代码改变世界

【设计模式】工厂方法模式 Factory Method Pattern

2018-07-24 13:35 by 蓝之风, ... 阅读, ... 评论, 收藏, 编辑

简单工厂模式中产品的创建统一在工厂类的静态工厂方法中创建,体现了面形对象的封装性,客户程序不需要知道产品产生的细节,也体现了面向对象的单一职责原则(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文件也扫描进去,不然当前程序集中实现的工厂无法找到。