【设计模式】简单工厂工厂方法抽象工厂

本文主要介绍工厂模式,首先是最基本的简单工厂(严格地说这不是标准的设计模式),然后是工厂方法模式和抽象工厂模式。

1. 简单工厂

2. 工厂方法模式

3. 抽象工厂模式

在这里共同使用的场景是一个数据转换的应用:某客户A要把自己电脑某程序中的数据导出,再导入给B,而导出数据的格式是不确定的,可以是Excel,可以是XML等等。

 

简单工厂

1. 面向接口的编程

在Java应用开发中,要“面向接口编程”,而接口的思想是“封装隔离”,这里的封装不是对数据的封装,而是指对被隔离体的行为或职责的封装,并把外部调用和内部实现隔离开,只要接口不变,内部实现的变化就不会影响到外部应用,从而使得系统更灵活,具有更好的扩展性和可维护性,“接口是系统可插拔性的保证”。

2. 不使用工厂模式时的接口使用方法

对上述场景做一下简化,这个转移数据的程序完成之后直接交由客户使用,不再进行二次开发,说得通俗一点,程序的接口已经指定,必须使用Excel或XML这两种格式之一进行导出,在使用本程序的时候,客户直接选择是哪一种格式,在这里,仅关心客户端A的行为,也就是怎么导出数据。

假设有一个接口叫IExportFile,实现类ExportExcel,实现了方法export(),那么创建实现该接口的实例的时候,会这样写,

首先是IExportFile接口的内容:

public interface IExportFile {
        public void export();
}

实现类ExportExcel的内容:

public class ExportExcel implements IExportFile {
        @Override
        public void export() {
           // TODO Auto-generated method stub
           System.out.println("输出excel格式数据...");
        }
}

在main函数中:

public static void main(String [] args){
       ExportExcel expFile = new ExportExcel();
       expFile.export();
}

这样的调用方式有一个不方便的地方,客户端在调用的时候,不仅仅使用了接口,还确切的知道了具体的实现类是哪个,试想,对于一个使用者而言,我只指定说我要excel格式的数据就可以了,还需要知道导出类的名字是ExportExcel吗?这就失去了使用接口的一部分意义(只实现了多态,而没有实现封装隔离),而且直接使用

ExportExcel expFile = new ExportExcel();

这样的语句就可以了。

如下图所示,客户端需要知道所有的模块:

而较为好的编程方式,是客户端只知道接口而不知道实现类是哪个,在这个例子中,客户端只知道是使用了IExportFile接口,而不关心具体谁去实现,也不知道是怎么实现的,怎么做到这一点呢,可以使用简单工厂来解决。

3. 简单工厂

定义说明:简单工厂提供一个创建对象实例的功能,而无须关心具体实现,被创建实例的类型可以是接口、抽象类或是具体的类,外部不应该知道实现类,而内部是必须要知道的,所以可以在内部创建一个类,在这个类的内部生成接口并返回给客户端。

具体实现方法:现在该接口有两个实现类:ExportExcel和ExportXML(内容与ExportExcel对应,不再给出具体实现),下面来看简单工厂的实现:

public class ExportFactory {
        public static IExportFile createExportFormat(int index){
           IExportFile expFile = null;
           if(index == 0){
               expFile = new ExportExcel();
           }
           else if(index == 1){
               expFile = new ExportXML();
           }
           return expFile;
        }
}

在客户端呢,通过传入参数来生成具体实现,这样只需要告诉客户数字与输出数据格式的对应关系,而不用让客户去知道实现类是哪个:

public static void main(String [] args){
        IExportFile expFile = ExportFactory.createExportFormat(0);
        expFile.export();
}

在客户端,已经被屏蔽了具体的实现:

4. 可配置的简单工厂

上面的例子中,有两个实现类,传入的参数index可以取值0或1,如果再增加一种实现类,就需要修改工厂类的方法,肯定不是一种好的实现方法,这里提供一种解决方案,通过java的反射来完成(我用反射写程序曾经被IBM的工程师批评过,因为尤其在继承关系比较复杂的情况下会出现一些安全问题,这也就是使用反射经常要捕获SecurityException的原因,所以要慎重选用),比如有一个xml或properties配置文件,为方便演示,这里使用properties文件,命名为factory.properties,里面有一行配置的属性:

ExportClass=ExportExcel(我的范例是在默认包里做的,正式工程中需要加适当的前缀,如org.zhfch.export.ExportExcel)

此时工厂类内容如下:

public class ExportFactory {
        public static IExportFile createExportFormat(){
            IExportFile expFile = null;
            Properties p = new Properties();
            try {
                p.load(Factory.class.getResourceAsStream("factory.properties"));
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            try {
                expFile = (IExportFile) Class.forName(p.getProperty("ExportClass")).newInstance();
            } catch (InstantiationException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            return expFile;
        }
}

此时,可以直接在配置文件中进行配置,无需再去在程序里传参了。

5. 模式说明

该类用于创造接口,因此命名为工厂,通常不需要创建工厂类实例,使用静态方法,这样的工厂也被称为静态工厂,而且简单工厂理论上可以创造任何类,也叫万能工厂,类名通常为“模块名+Factory”,如MultimediaFactory,方法名通常为“get+接口名称”或“create+接口名称”。

工厂类仅仅用来选择实现类,而不创造具体实现方法,而具体选用哪个实现类,可以选用函数传参、读取配置文件、读取程序运行的某中间结果来选择。

简单工厂的优点是比较好地实现了组件封装,同时降低了客户端与实现类的耦合度,缺点是客户端在配置的时候需要知道许多参数的意义,增加了复杂度,另外如果想对工厂类进行继承来覆写创建方法,就不能够实现了。

扩展概念-抽象工厂模式:抽象工厂里面有多个用于选择并创建对象的方法,并且创建的这些对象之间有一定联系,共同构成一个产品簇所需的部件,如果抽象工厂退化到只有一个实现,就是简单工厂了。

扩展概念-工厂方法模式:工厂方法模式把选择实现的功能放到子类里去实现,如果放在父类里就是简单工厂。

工厂方法模式(Factory Method

1. 框架的相关概念

框架就是能完成一定功能的半成品软件,它不能完全实现用户需要的功能,需要进一步加工,才能成为一个满足用户需要的、完整的软件。框架级的软件主要面向开发人员而不是最终用户。

使用框架能加快应用开发进度,并且能提供一个精良的程序架构。

而设计模式是一个比框架要抽象得多的概念(框架已经是一个产品,而设计模式还是个思想),框架目的明确,针对特定领域,而设计模式更加注重解决问题的思想方法。

2. 工厂方法模式

现在把简单工厂的那个场景稍作变化,现在只是做一个易于扩展的程序框架,在编写导出文件的行为时,我们并不知道具体要导出成什么文件,首先有一些约定俗成的格式,比如Excel、XML,但也可能是txt,甚至是现在还完全想不到的格式,换句话说,即使在这个半成品软件的内部,也不一定知道该去选择什么实现类,这个时候,就可以使用工厂方法模式。

工厂方法模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类,Factory Method将一个类的实例化延迟到子类中进行。

在这个场景中,我们需要的导出文件的接口,仍然是IExportFile:

public interface IExportFile {
    public void export();
}

先来看ExportExcel类的实现:

public class ExportExcel implements IExportFile {
    @Override
    public void export() {
       // TODO Auto-generated method stub
       System.out.println("输出excel格式数据...");
    }
}

重点是一个生成器类,在该类中声明工厂方法,这个工厂方法通常是protected类型,返回一个IExportFile类型的实例化对象并在这个类的其他方法中被使用,而这个工厂方法多是抽象的,在子类中去返回实例化的IExportFile对象:

public abstract class ExportCreator {
    protected abstract IExportFile factoryMethod();
    public void export(){
       IExportFile expFile = factoryMethod();
       expFile.export();
    }
}

这样,这个抽象类的export()方法在不知道具体实现的情况下实现了数据的导出操作。如果这时候要使用Excel这种导出格式,在已经有IExportFile对应的实现类ExportExcel之后(没有的话就先创建),再创建一个ExportCreator的子类,覆写factoryMethod方法来返回ExportExcel的对象实例就可以了:

在ExportCreator的子类中选择IExportFile的实现:

public class ExportExcelCreator extends ExportCreator{
    @Override
    protected IExportFile factoryMethod() {
       // TODO Auto-generated method stub
       return new ExportExcel();
    }
}

使用时在main函数中调用:

public static void main(String [] args){
    ExportCreator ec = new ExportExcelCreator();
    ec.export();
}

模式的类结构图如下:

3. 工厂方法模式与IoC/DI

所谓的控制反转/依赖注入,要理解:是某个对象依赖于IoC/DI容器来提供外部资源,或者说IoC/DI容器向某个对象注入外部资源,IoC/DI容器控制对象实例的创建。比如有一个操作文件格式的逻辑类:FileFormatLogic,该类中有一个FileFormatDAO类型的变量fileFormatDao,那么此时,fileFormatDao就是FileFormatLogic所需的外部资源,正常思路是在FileFormatLogic中使用fileFormatDao = new FileFormatDAO()来创建对象,这是正向的,而反转是说,FileFormatLogic不再主动地去创建对象,而是被动的等IoC/DI容器给它一个FileFormatDAO的对象实例。

工厂方法模式与IoC/DI的思想有类似的地方:

现在有一个类A,需要一个接口C的实例,并使用依赖注入的方法获得,A代码如下:

public class A {
    //等待被注入的对象c
    private C c = null;
    //注入对象c的方法
    public void setC(C c){
       this.c = c;
    }
    //使用从外部注入的c做一些事情
    public void ta(){
       c.tc();
    }
}

接口C很简单:

public interface C {
    public void tc();
}

那么怎么能把IoC/DI和工厂方法模式的思想联系到一起呢?需要对A做一点改动,现在修改A的内容:

public abstract class A {
    //需要C实例时调用,相当于从子类注入
    protected abstract C createC();
    //需要使用C实例时,调用方法让子类提供一个
    public void ta(){
       createC().tc();
    }
}

createC()就是一个工厂方法,等待在子类中进行注入(这和我们常用的依赖注入并不相同,思想相似)。

4. 参数化的工厂方法

前面的例子中,我们使用的工厂方法都是抽象的,但它必须抽象吗?其实不是的,我们可以在工厂方法中提供一些默认的实例选择(通过判断传入的index参数生成不同的实例),需要扩展时再在子类中进行覆写,参数化的创建器如下:

public class ExportCreator {
    protected IExportFile factoryMethod(int index){
       IExportFile expFile = null;
       if(index == 0){
           expFile = new ExportExcel();
       }
       else if(index == 2){
           expFile = new ExportXML();
       }
       return expFile;
    }
    public void export(int index){
       IExportFile expFile = factoryMethod(index);
       expFile.export();
    }
}

如果这个时候突然又需要导出Txt格式的数据,则需要继承这个创建器类,覆写工厂方法,特殊的地方是,如果传入的参数指示并不是txt的实现,则调用父类的默认方法来选择对象:

public class ExportTxtCreator extends ExportCreator{
    @Override
    protected IExportFile factoryMethod(int index) {
       // TODO Auto-generated method stub
       IExportFile expFile = null;
       if(index == 2){
           expFile = new ExportTxt();
       }
       else{
           expFile = super.factoryMethod(index);
       }
       return expFile;
    }
}

5. 模式说明

工厂方法的本质就是把选择实现方式延迟到子类来完成,它的优点是可以在不知道具体实现的情况下编程、易于扩展,缺点是具体产品对象和工厂方法是耦合的。

看最后“参数化的工厂方法”中的ExportCreator创建器的实现,如果把export方法去掉,再为工厂方法加上static修饰,就变成了简单工厂,他们本质上是类似的。

何时选用:如果一个类需要创建某个接口的对象,但又不知道具体的实现,或者本来就希望子类来创建所需对象的时候选用。

抽象工厂模式(Abstract Factory

1. 场景描述

对于最初的场景,在简单工厂和工厂方法中,都只是使用了客户A的导出,现在要考虑在客户B那里导入了,这两个模块分别由A和B各自实现,我们可以仿照简单工厂中的代码来完成导入功能,但很容易想到问题:A用Excel格式导出,B却调用XML格式导入怎么办?

2. 抽象工厂模式

上面描述的问题出现根源是:导入和导出这两个模块是相互依赖的,而抽象工厂就是要提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的实现类。

换言之,抽象工厂主要起一个约束作用,提供所有子类的一个统一的外观,来让客户使用。

导出导入的接口如下:

public interface IExportFile {
    public void export();
}
public interface IImportFile {
    public void iimport();//import是关键字,所以加一个i
}

抽象工厂(是一个接口)定义如下:

public interface AbstractFactory {
    public IExportFile createExport();
    public IImportFile createImport();
}

为每一种数据转移方案添加一种具体实现,数据转移方案1:

public class Schema1 implements AbstractFactory {
    @Override
    public IExportFile createExport() {
       // TODO Auto-generated method stub
       return new ExportExcel();
    }
    @Override
    public IImportFile createImport() {
       // TODO Auto-generated method stub
       return new ImportExcel();
    }
}

数据转移方案2:

public class Schema2 implements AbstractFactory {
    @Override
    public IExportFile createExport() {
       // TODO Auto-generated method stub
       return new ExportXML();
    }
    @Override
    public IImportFile createImport() {
       // TODO Auto-generated method stub
       return new ImportXML();
    }
}

对于一个全局的数据转移的类,接收一个指定的数据转移方案作为参数来进行数据转移:

public class DataTransfer {
    public void transfer(AbstractFactory schema){
       //当然应该把导出的内容传给导入的模块,此处从略了
       schema.createExport().export();
       schema.createImport().iimport();
    }
}

使用时创建一个具体的解决方案并传给这个类去进行处理:

public static void main(String [] args){
    DataTransfer dt = new DataTransfer();
    AbstractFactory schema = new Schema1();
    dt.transfer(schema);
}

模式结构图如下:

3. 抽象工厂模式与DAO

DAO是J2EE中的一个标准模式,解决访问数据对象所面临的诸如数据源不同、存储类型不同、数据库版本不同等一系列问题。对逻辑层来说,他可以直接访问DAO而不用关心这么多的不同,换言之,借助DAO,逻辑层可以以一个统一的方式来访问数据。事实上,在实现DAO时,最常见的实现策略就是工厂,尤以抽象工厂居多。

比如订单处理的模块,订单往往分为订单主表和订单明细表,现在业务对象需要操作订单的主记录和明细记录,而数据底层的数据存储方式可能是不同的,比如可能是使用关系型数据库来存储,也可能是使用XML来进行存储。说到这里就很容易和前面的例子结合在一起了吧?原理可以说是完全一样的,这里给出抽象工厂实现策略的结构示意图,就不再给出代码实现了:

4. 可扩展的抽象工厂

现在的抽象工厂,如果要进行扩展,比如在数据导出和导入之间要加一个数据清洗的模块,就比较麻烦了,从接口到每一个实现都需要添加一个新的方法,有一种较为灵活的实现方式,但是却有一定安全问题。

首先,抽象工厂不是要为每一个模块返回一个实例吗,每增加一个模块就要增加一个方法不易于扩展,那么就干脆只用一个方法,根据参数来返回不同的类型,再强制转化成我们需要的模块对象,显而易见,这时候抽象工厂这个唯一的方法就需要返回一个Object类型的对象了:

public interface AbstractFactory {
    public Object createModule(int module);
}

在具体的实现方案中,就要根据参数返回不同的模块实例,比如在方案1(Excel格式的数据转移方案)中,可以指定,参数为0时返回Excel导出模块的实例,参数为1时返回Excel导入模块的实例:

public class Schema1 implements AbstractFactory {
    @Override
    public Object createModule(int module) {
       // TODO Auto-generated method stub
       Object obj = null;
       if(module == 0){
           obj = new ExportExcel();
       }
       else if(module == 1){
           obj = new ImportExcel();
       }
       /**
        * 如果此事要添加一个清晰数据的模块,则可以在这里添加
        * else if(module == 2){
        *     obj = new CleanExcel();
        * }
        */
       return obj;
    }
}

数据处理的全局类修改如下:

public class DataTransfer {
    public void transfer(AbstractFactory schema){
       //当然应该把导出的内容传给导入的模块,此处从略了
       ((IExportFile)schema.createModule(0)).export();
       /**
        * 添加清洗模块时在这里添加:
        * ((ICleanFile)schema.createModule(2)).clean();
        */
       ((IImportFile)schema.createModule(1)).iimport();
    }
}

main函数的调用方式不变,所谓的不安全就是指,如果指定返回参数0所对应的对象(IExportFile),但是却强制转化成IImportFile,就会抛异常,但这种方法确实比之前的方法灵活了许多,是否应该选用就要看具体应用设计上的权衡了。

5. 模式说明

AbstractFactory通常是一个接口,而不是抽象类(也可以实现为抽象类,但是不建议)!而在AbstractFactory中创建对象的方式,可以看做是工厂方法,这些工厂方法的具体实现延迟到子类具体的工厂中去,换句话说,经常会使用工厂方法来实现抽象工厂。

切换产品簇:抽象工厂的一系列对象通常是相互依赖或相关的,这些对象就构成一个产品簇,切换产品簇只需要提供不同的抽象工厂的实现就可以了。把产品簇作为一个整体来进行切换。甚至可以说,抽象工厂模式的本质,就是选择产品簇的实现。

抽象工厂模式的优点:分离了接口和实现,使得切换产品簇非常方便。

抽象工厂模式的缺点:不易扩展(使用前面提到的扩展方法又不够安全),使得类层次结构变得复杂。

通常一个产品系列只需要一个实例就够了,所以具体的工厂实现可以用单例模式来实现。

注:参考书目为清华大学出版社出版的《研磨设计模式》一书。

原文地址:https://www.cnblogs.com/smarterplanet/p/2712812.html