代码改变世界

【设计模式】抽象工厂模式 Abstract Factory Pattern

2018-07-30 08:15 by 蓝之风, ... 阅读, ... 评论, 收藏, 编辑

简单工厂模式是一个工厂类根据工厂方法的参数创建不出不同的产品, 工厂方法模式是每一个产品都有一个一一对应的工厂负责创建该产品。那么今天要讲的抽象工厂模式是一个工厂能够产生关联的一系列产品。抽象工厂模式相对于简单工厂和工厂方法模式来着更具抽象性。

一、抽象工厂模式演绎

我们先来看一个简单的需求: 甲方要开发一套办公自动化软件,其中有一个非常重要的功能就是要能够导入Word 文档和Excel 文档。

开发人员拿到需求后就开始编码了,  很快代码写完了:

public  class ImportTool
{
    public void ImportWord()
    {
        Console.WriteLine("Import Word");
    }

    public void ImportExcel()
    {
        Console.WriteLine("Import Excel");
    }
}

客户端调用代码:

class Program
{
    static void Main(string[] args)
    {
        ImportTool importTool = new ImportTool();
        importTool.ImportWord();
        importTool.ImportExcel();

        Console.ReadKey();
    }
}

输出结果:

image

看起来不错, 但是看代码,客户端的代码和具体的实现之间是直接new出来的 ,简直就是面向具体编程,没有接口没有抽象,是不是违背了ISP原则了?,那好开发人员决定提出一个抽象层,提出一个抽象的文档接口,   用简单工厂模式来实现这个需求:

UML 图如下:

 image

代码如下:

public interface IDocument
{
    void Import();
}    
public class WordDocument : IDocument
{    
    public void Import()
    {
       Console.WriteLine("Import Word");
    }
}
public class ExcelDocument : IDocument
{
    public void Import()
    {
       Console.WriteLine("Import Excel");
    }
}

public class DocumentFactory
{
    public static IDocument Create(string documentType)
    {
        IDocument document;
        switch (documentType)
        {
            case "word":
                document = new WordDocument();
                break;
            case "excel":
                document = new ExcelDocument();
                break;
            default:
                throw new ArgumentException("Invalid argument: documentType");
        }

        return document;
    }
}

客户端调用:

static void Main(string[] args)
{
    IDocument document=DocumentFactory.Create("word");
    document.Import();
        
    document=DocumentFactory.Create("excel");
    document.Import();

    Console.ReadKey();
}

输出:

image

看起来完美这是一个标准的静态工厂模式的实现。

第一次需求变更: 增加 对Power Point 的导入支持

 

 

增加了一个产品相当于,因为之前应用了简单工厂模式现在改起来很简单。

UML 图:

image

代码:

public interface IDocument
{
    void Import();
}    
public class WordDocument : IDocument
{    
    public void Import()
    {
 	        Console.WriteLine("Import Word");
    }
}
public class ExcelDocument : IDocument
{
        public void Import()
    {
 	    Console.WriteLine("Import Excel");
    }
}
public class PowerPointDocument : IDocument
{
    public void Import()
    {
        Console.WriteLine("Import Power Point");
    }
}

public class DocumentFactory
{
    public static IDocument Create(string documentType)
    {
        IDocument document;
        switch (documentType)
        {
            case "word":
                document = new WordDocument();
                break;
            case "excel":
                document = new ExcelDocument();
                break;
            case "powerpoint":
                document = new PowerPointDocument();
                break;
            default:
                throw new ArgumentException("Invalid argument: documentType");
        }

        return document;
    }
}

客户端调用:

static void Main(string[] args)
{
    IDocument document=DocumentFactory.Create("word");
    document.Import();
        
    document=DocumentFactory.Create("excel");
    document.Import();

    document = DocumentFactory.Create("powerpoint");
    document.Import();
    Console.ReadKey();
}

输出结果:

image

 

没问题,一起都在控制之中。

第二次需求变更: 支持office 2007 以后的文档格式。

Offcie 2007是个坎,2007之前的文档格式和2007以后的文档格式不一 样, word 在2007前的文档后缀是.doc, 2007 及以后就变成.docx了这个看似简单的需求实则是增加了小一半的工作量啊,新的格式的文档要重新进行解码才能拿到正确的数据,拿刚刚实现的简单工厂设计模式来应对这次变更就要新增加3个产品类并且要工厂来创建创建者三个类的实例。看来静态工厂已经不能再适应这一次的需求变化了,会导致静态工厂方法的逻辑变得异常复杂难以维护。那用工厂方法模式来解决这个问题,工厂方法刚好可以将产品的创建工作提取到单独的工厂中去完成,重构下工厂将静态工厂模式替换成工厂方法模式:

UML 图:

image 代码:

public interface IDocument
{
    void Import();
}
public interface IDocumentFactory
{
    IDocument Create();
}
public class WordDocument : IDocument
{    
    public void Import()
    {
 	        Console.WriteLine("Import Word");
    }
}
public class ExcelDocument : IDocument
{
        public void Import()
    {
 	    Console.WriteLine("Import Excel");
    }
}
public class PowerPointDocument : IDocument
{
    public void Import()
    {
        Console.WriteLine("Import Power Point");
    }
}

public class WordXDocument : IDocument
{
    public void Import()
    {
        Console.WriteLine("Import WordX");
    }
}
public class ExcelXDocument : IDocument
{
    public void Import()
    {
        Console.WriteLine("Import ExcelX");
    }
}
public class PowerPointXDocument : IDocument
{
    public void Import()
    {
        Console.WriteLine("Import Power PointX");
    }
}

public class WordDocumentFactory : IDocumentFactory
{
    public  IDocument Create()
    {
        return new WordDocument();
    }
}
public class WordXDocumentFactory : IDocumentFactory
{
    public IDocument Create()
    {
        return new WordXDocument();
    }
}

public class ExcelDocumentFactory : IDocumentFactory
{
    public IDocument Create()
    {
        return new ExcelDocument();
    }
}
public class ExcelXDocumentFactory : IDocumentFactory
{
    public IDocument Create()
    {
        return new ExcelXDocument();
    }
}
public class PowerPointDocumentFactory : IDocumentFactory
{
    public IDocument Create()
    {
        return new PowerPointDocument();
    }
}
public class PowerPointXDocumentFactory : IDocumentFactory
{
    public IDocument Create()
    {
        return new PowerPointXDocument();
    }
}

客户端调用代码:

static void Main(string[] args)
{
    IDocument document;
    IDocumentFactory documentFactory;

    documentFactory = new WordDocumentFactory();
    document = documentFactory.Create();
    document.Import();

    documentFactory = new WordXDocumentFactory();
    document = documentFactory.Create();
    document.Import();

    documentFactory = new ExcelDocumentFactory();
    document = documentFactory.Create();
    document.Import();

    documentFactory = new ExcelXDocumentFactory();
    document = documentFactory.Create();
    document.Import();

    documentFactory = new PowerPointDocumentFactory();
    document = documentFactory.Create();
    document.Import();

    documentFactory = new PowerPointXDocumentFactory();
    document = documentFactory.Create();
    document.Import();
   
    Console.ReadKey();
}

输出结果:

image

 

一个产品一个实现类,一个工厂类,这样职责单一符合SRP,但是系统中的类在成倍的增加,有点复杂了,如果在增加一个系列的产品那还了得。

那么能不能减少一些类呢?经过分析我们发现,这些导入的文档中2007之前的一系列文档的解析规则基本类似实现的技术也是类似的,2007及以后的文档的解析规则类似。所以我们可以把这些产品分成两个系列,2007之前的成为Document系列,2007以后的文档成为DocumentX系列, 那么我们就可以创建两个具体的工厂来创建Document系列和DocumentX系列, 从另一个维度来看,Word 和WordX, Excel 和ExcelX,PowerPoint 和 PowerPointX的的关系也很密切,因为都是同一个产品,只是处在不同的系列上,他们各自的编码又各自类似,因此在这个维度上可以将其提出一组新的接口,IWordDocument 用于处理word的导入(Word和WordX),IExcelDocument 用处理Excel的导入(Excel 和ExcelX),IPowerPoint用于处理 PowerPoint 导入(PowerPoint和PowerPointX),根据这个思路重构代码:

UML 图

image

代码:

public interface IWordDocument
{
    void Import();
}
public interface IExcelDocument
{
    void Import();
}
public interface IPowerPointDocument
{
    void Import();
}
public interface IDocumentFactory
{
    IWordDocument CreateWord();
    IExcelDocument CreateExcel();
    IPowerPointDocument CreatePowerPoint();
}
public class WordDocument : IWordDocument
{    
    public void Import()
    {
 	        Console.WriteLine("Import Word");
    }
}
public class WordXDocument : IWordDocument
{
    public void Import()
    {
        Console.WriteLine("Import WordX");
    }
}
public class ExcelDocument : IExcelDocument
{
    public void Import()
    {
 	    Console.WriteLine("Import Excel");
    }
}
public class ExcelXDocument : IExcelDocument
{
    public void Import()
    {
        Console.WriteLine("Import ExcelX");
    }
}
public class PowerPointDocument : IPowerPointDocument
{
    public void Import()
    {
        Console.WriteLine("Import Power Point");
    }
}
public class PowerPointXDocument : IPowerPointDocument
{
    public void Import()
    {
        Console.WriteLine("Import Power PointX");
    }
}
public class DocumentFactory : IDocumentFactory
{
    public IWordDocument CreateWord()
    {
        return new WordDocument();
    }

    public IExcelDocument CreateExcel()
    {
        return new ExcelDocument();
    }

    public IPowerPointDocument CreatePowerPoint()
    {
        return new PowerPointDocument();
    }
}
public class DocumentXFactory : IDocumentFactory
{
    public IWordDocument CreateWord()
    {
        return new WordXDocument();
    }

    public IExcelDocument CreateExcel()
    {
        return new ExcelXDocument();
    }

    public IPowerPointDocument CreatePowerPoint()
    {
        return new PowerPointXDocument();
    }
}

客户端调用:

static void Main(string[] args)
{
    IWordDocument wordDocument;
    IExcelDocument excelDocument;
    IPowerPointDocument powerPointDocument;

    IDocumentFactory documentFactory;

    documentFactory = new DocumentFactory();
    wordDocument = documentFactory.CreateWord();
    excelDocument = documentFactory.CreateExcel();
    powerPointDocument = documentFactory.CreatePowerPoint();
    wordDocument.Import();
    excelDocument.Import();
    powerPointDocument.Import();

    documentFactory = new DocumentXFactory();
    wordDocument = documentFactory.CreateWord();
    excelDocument = documentFactory.CreateExcel();
    powerPointDocument = documentFactory.CreatePowerPoint();
    wordDocument.Import();
    excelDocument.Import();
    powerPointDocument.Import();    
   
    Console.ReadKey();
}

输出:

image

这一次工厂的数量得到了控制,这里只有两个工厂类就搞定了,DocumentFactory 负责创建office 2007之前的文档对象,DocumentXFactory 负责创建 office 2007以及以后的文档对象。这就解决了工厂方法模式工厂类随着产品增加随之增加带来的复杂性,以及不易维护的问题。这样如果在增加一系列产品就变得容易了很多,只需要再创建一个系列产品的具体工厂并继承自抽象工厂,以及实现系列产品的抽象接口的具体类就可以了。

随着需求的变化一步一步的经过重构代码也一步一步从简单工厂模式到工厂方法模式再到抽象工厂模式了。现在文档处理代码就是一个典型的抽象工厂模式了,那么下面来看看抽象工厂模式到底是什么?

二、抽象工厂模式定义:

抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,它是一种对象创建型模式。在抽象工厂模式中,每一个具体工厂都提供了多个工厂方法用于产生多种不同类型的产品,这些产品构成了一个产品族(产品系列).

三、工厂方法模式结构图:

image 抽象工厂模式结构图

1、AbsctactFactory(抽象抽象工厂):

它声明了一组用于创建一系列产品的方法,每一个方法对应创建一种产品。

2.ConcreteFactory(具体工厂):

它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品系列,每个产品都在同一个系列中。

3. AbsctactProdcut(抽象产品):

它为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法。

4.ConcreteProdcut(具体产品):

它定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法。

四、抽象工厂模式代码实现

在抽象工厂中声明了多个工厂方法,用于创建不同类型的产品,抽象工厂可以是接口,也可以是抽象类或者具体类,其典型代码如下:

public interface IAbstractProdcutA
{
    void DoSomething();
}
public interface IAbstractProdcutB
{
    void DoSomething();
}
public interface IAbstractFactory{
    IAbstractProdcutA CreateProductA();
    IAbstractProdcutB CreateProductB();
}

public class ConcreteProductA1:IAbstractProdcutA
{
    public void DoSomething()
    {
 	    Console.WriteLine("I'm ConcreteProductA1");
    }
}
public class ConcreteProductA2:IAbstractProdcutA
{
    public void DoSomething()
    {
 	    Console.WriteLine("I'm ConcreteProductA2");
    }
}

public class ConcreteProductB1:IAbstractProdcutB
{
    public void DoSomething()
    {
 	    Console.WriteLine("I'm ConcreteProductB1");
    }
}
public class ConcreteProductB2:IAbstractProdcutB
{
    public void DoSomething()
    {
 	    Console.WriteLine("I'm ConcreteProductB2");
    }
}

public class ConcreteFactory1 : IAbstractFactory
{
    public IAbstractProdcutA CreateProductA()
    {
        return new ConcreteProductA1();
    }
    public IAbstractProdcutB CreateProductB()
    {
        return new ConcreteProductB1();
    }
}

public class ConcreteFactory2 : IAbstractFactory
{
    public IAbstractProdcutA CreateProductA()
    {
        return new ConcreteProductA2();
    }
    public IAbstractProdcutB CreateProductB()
    {
        return new ConcreteProductB2();
    }
}

客户端调用代码:

static void Main(string[] args)
{    
    IAbstractProdcutA productA;
    IAbstractProdcutB productB;

    IAbstractFactory factory = new ConcreteFactory1();
    productA = factory.CreateProductA();
    productB = factory.CreateProductB();
    productA.DoSomething();
    productB.DoSomething();

    factory = new ConcreteFactory2();
    productA = factory.CreateProductA();
    productB = factory.CreateProductB() ;
    productA.DoSomething();
    productB.DoSomething();
   
    Console.ReadKey();
}

输出:

image

五 、抽象工厂模式的优点

  1. 抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。比如常用的配置+反射就可轻松替换掉工厂进而替换掉整个以为逻辑。

  2. 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品系列中的对象。

  3. 增加新的产品系列很方便,无须修改已有系统,只需要增加具体产品类和具体工厂就可以了符合“开闭原则(OCP)“。

六、抽象工厂模式的缺点

  1. 增加新的产品麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则(OCP)“。如果新增一个产品就要在抽象工厂中增加一个创建该产品的方法。这样就牵一发动全身,原来的所有集成该抽象工厂的具体工厂都要修改。

七、抽象工厂模式的使用场景

  1. 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是很重要的,用户无须关心对象的创建过程,将对象的创建和使用解耦。
  2. 系统中有多于一个的产品系列,而每次只使用其中某一产品系列。可以通过配置文件等方式来使得用户可以动态改变产品系列,也可以很方便地增加新的产品系列。
  3. 属于同一个产品系列的产品将在一起使用,这一约束必须在系统的设计中体现出来。同一个产品系列中的产品可以是没有任何关系的对象,但是它们都具有一些共同的约束,如Office 文档的支持,在window xp  重视不能使用Office 2007 +的,window 7 支持0ffice 2007,因此这个约束就是操作系统对office版本的支持。
  4. 产品类型稳定,设计完成之后,不会向系统中增加新的产品类型或者删除已有的产品产品类型。

八、扩展

第三次需求改变:从商业化上考虑甲方要求这个软件的某些版本只提供导入office 2007 之前的文件,某些版本只支持2007以后版本。

怎么办呢? 这个需求开发人员拿到后一定窃喜,抽象工厂实现这种需求简直是太容易了。配置+反射呀, 将具体工厂配置在配置文件中,代码中只是用抽象并通过反射来创建工厂实进而达到动态控制的能力。

如果软件版本中V1中只要支持office 2007以前的文档版本。那么在配置文件App.config  增加以下节点:

image

配置完节点后在代码中通过反射来创建工厂类, 代码如下

static void Main(string[] args)
{
    IWordDocument wordDocument;
    IExcelDocument excelDocument;
    IPowerPointDocument powerPointDocument;

    IDocumentFactory documentFactory;

    // 读取配置文件并且通过反射创建工厂实例
    var setting = ConfigurationSettings.AppSettings["DocumentFaccory"];
    var obj = Type.GetType(setting);
    if (obj == null) return;
    documentFactory = Activator.CreateInstance(obj) as IDocumentFactory;

    if (documentFactory == null) return;
 
    wordDocument = documentFactory.CreateWord();
    excelDocument = documentFactory.CreateExcel();
    powerPointDocument = documentFactory.CreatePowerPoint();
    wordDocument.Import();
    excelDocument.Import();
    powerPointDocument.Import();
   
    Console.ReadKey();
}

输出结果如下:

image

如果在V1.1版本中甲方提出要求只支持office 2007以后的版本,那么只需要修改配置文件如下就可以了:

把原来的

<appSettings>
    <add key="DocumentFaccory" value="DesignPattern.AbstractFactory.DocumentFactory"/>
</appSettings>

改成:

<appSettings>
    <add key="DocumentFaccory" value="DesignPattern.AbstractFactory.DocumentXFactory"/>
</appSettings>

其它地方不用做任何修改,输出结果:

image