设计模式-工厂方法(转)

工厂方法模式(Factory Method)

1  场景问题

1.1  导出数据的应用框架

        考虑这样一个实际应用:实现一个导出数据的应用框架,来让客户选择数据的导出方式,并真正执行数据导出。
        在一些实际的企业应用中,一个公司的系统往往分散在很多个不同的地方运行,比如各个分公司或者是门市点,公司没有建立全公司专网的实力,但是又不愿意让业务数据实时的在广域网上传递,一个是考虑数据安全的问题,一个是运行速度的问题。
        这种系统通常会有一个折中的方案,那就是各个分公司内运行系统的时候是独立的,是在自己分公司的局域网内运行。然后在每天业务结束的时候,各个分公司会导出自己的业务数据,然后把业务数据打包通过网络传送给总公司,或是专人把数据送到总公司,然后由总公司进行数据导入和核算。
        通常这种系统,在导出数据上,会有一些约定的方式,比如导出成:文本格式、数据库备份形式、Excel格式、Xml格式等等。
        现在就来考虑实现这样一个应用框架。在继续之前,先来了解一些关于框架的知识。

1.2  框架的基础知识

(1):框架是什么
        简单点说:框架就是能完成一定功能的半成品软件
        就其本质而言,框架是一个软件,而且是一个半成品的软件。所谓半成品,就是还不能完全实现用户需要的功能,框架只是实现用户需要的功能的一部分,还需要进一步加工,才能成为一个满足用户需要的、完整的软件。因此框架级的软件,它的主要客户是开发人员,而不是最终用户。
        有些朋友会想,既然框架只是个半成品,那何必要去学习和使用框架呢?学习成本也不算小,那就是因为框架能完成一定的功能,也就是这“框架已经完成的一定的功能”在吸引着开发人员,让大家投入去学习和使用框架。


(2):框架能干什么
        能完成一定功能,加快应用开发进度
        由于框架完成了一定的功能,而且通常是一些基础的、有难度的、通用的功能,这就避免我们在应用开发的时候完全从头开始,而是在框架已有的功能之上继续开发,也就是说会复用框架的功能,从而加快应用的开发进度。
        给我们一个精良的程序架构
        框架定义了应用的整体结构,包括类和对象的分割,各部分的主要责任,类和对象怎么协作,以及控制流程等等。
现在Java界大多数流行的框架,大都出自大师手笔,设计都很精良。基于这样的框架来开发,一般会遵循框架已经规划好的结构来进行开发,从而让我们开发的应用程序的结构也相对变得精良了。

(3):对框架的理解
        基于框架来开发,事情还是那些事情,只是看谁做的问题
        对于应用程序和框架的关系,可以用一个图来简单描述一下,如图1所示:

如果没有框架,那么客户要求的所有功能都由开发人员自己来开发,没问题,同样可以实现用户要求的功能,只是开发人员的工作多点。
        如果有了框架,框架本身完成了一定的功能,那么框架已有的功能,开发人员就可以不做了,开发人员只需要完成框架没有的功能,最后同样是完成客户要求的所有功能,但是开发人员的工作就减少了。
        也就是说,基于框架来开发,软件要完成的功能并没有变化,还是客户要求的所有功能,也就是“事情还是那些事情”的意思。但是有了框架过后,框架完成了一部分功能,然后开发人员再完成一部分功能,最后由框架和开发人员合起来完成了整个软件的功能,也就是看这些功能“由谁做”的问题。

         基于框架开发,可以不去做框架所做的事情,但是应该明白框架在干什么,以及框架是如何实现相应功能的
        事实上,在实际开发中,应用程序和框架的关系,通常都不会如上面讲述的那样,分得那么清楚,更为普遍的是相互交互的,也就是应用程序做一部分工作,然后框架做一部分工作,然后应用程序再做一部分工作,然后框架再做一部分工作,如此交错,最后由应用程序和框架组合起来完成用户的功能要求。
        也用个图来说明,如图2所示:

图2  应用程序和框架的关系示意图

 

 如果把这个由应用程序和框架组合在一起构成的矩形,当作最后完成的软件。试想一下,如果你不懂框架在干什么的话,相当于框架对你来讲是个黑盒,也就是相当于在上面图2中,去掉框架的两块,会发现什么?没错,剩下的应用程序是支离破碎的,是相互分隔开来的。
        这会导致一个非常致命的问题,整个应用是如何运转起来的,你是不清楚的,也就是说对你而言,项目已经失控了,从项目管理的角度来讲,这是很危险的。
        因此,在基于框架开发的时候,虽然我们可以不去做框架所做的事情,但是应该搞明白框架在干什么,如果条件许可的话,还应该搞清楚框架是如何实现相应功能的,至少应该把大致的实现思路和实现步骤搞清楚,这样我们才能整体的掌控整个项目,才能尽量减少出现项目失控的情况。

 

(4):框架和设计模式的关系
        设计模式比框架更抽象
       框架已经是实现出来的软件了,虽然只是个半成品的软件,但毕竟是已经实现出来的了。而设计模式的重心还在于解决问题的方案上,也就是还停留在思想的层面。因此设计模式比框架更为抽象。
      

       设计模式是比框架更小的体系结构元素
       如上所述,框架是已经实现出来的软件,并实现了一系列的功能,因此一个框架,通常会包含多个设计模式的应用。

        框架比设计模式更加特例化
       框架是完成一定功能的半成品软件,也就是说,框架的目的很明确,就是要解决某一个领域的某些问题,那是很具体的功能,不同的领域实现出来的框架是不一样的。
      而设计模式还停留在思想的层面,在不同的领域都可以应用,只要相应的问题适合用某个设计模式来解决。因此框架总是针对特定领域的,而设计模式更加注重从思想上,从方法上来解决问题,更加通用化。

 

1.3  有何问题
       分析上面要实现的应用框架,不管用户选择什么样的导出格式,最后导出的都是一个文件,而且系统并不知道究竟要导出成为什么样的文件,因此应该有一个统一的接口,来描述系统最后生成的对象,并操作输出的文件。
       先把导出的文件对象的接口定义出来,示例代码如下:

 

/**

 * 导出的文件对象的接口

 */

public interface ExportFileApi {

    /**

     * 导出内容成为文件

     * @param data 示意:需要保存的数据

     * @return 是否导出成功

     */

    public boolean export(String data);

}

 

 对于实现导出数据的业务功能对象,它应该根据需要来创建相应的ExportFileApi的实现对象,因为特定的ExportFileApi的实现是与具体的业务相关的。但是对于实现导出数据的业务功能对象而言,它并不知道应该创建哪一个ExportFileApi的实现对象,也不知道如何创建。
        也就是说:对于实现导出数据的业务功能对象,它需要创建ExportFileApi的具体实例对象,但是它只知道ExportFileApi接口,而不知道其具体的实现。那该怎么办呢?

 

2  解决方案

图3  工厂方法模式结构示意图

图4  使用工厂模式来实现示例的程序结构示意图
 下面一起来看看代码实现。
(1)导出的文件对象接口ExportFileApi的实现没有变化,这里就不去赘述了
(2)接下来看看接口ExportFileApi的实现,为了示例简单,只实现导出文本文件格式和数据库备份文件两种。先看看导出文本文件格式的实现,示例代码如下:

(4)此时的客户端,非常简单,直接使用ExportOperate类,示例代码如下:

 

 

public class Client {

    public static void main(String[] args) {

        //创建需要使用的Creator对象

       ExportOperate operate = new ExportOperate();

       //调用输出数据的功能方法,传入选择到处类型的参数

       operate.export(1,"测试数据");

    }

}

       测试看看,然后修改一下客户端的参数,体会一下通过参数来选择具体的导出实现的过程。这是一种很常见的参数化工厂方法的实现方式,但是也还是有把参数化工厂方法实现成为抽象的,这点要注意,并不是说参数化工厂方法就不能实现成为抽象类了。只是一般情况下,参数化工厂方法,在父类都会提供默认的实现。

(5)扩展新的实现
        使用参数化工厂方法,扩展起来会非常容易,已有的代码都不会改变,只要新加入一个子类来提供新的工厂方法实现,然后在客户端使用这个新的子类即可。
        这种实现方式还有一个有意思的功能,就是子类可以选择性覆盖,不想覆盖的功能还可以返回去让父类来实现,很有意思。
        先扩展一个导出成xml文件的实现,试试看,示例代码如下: 

/**

 * 导出成xml文件的对象

 */

public class ExportXml implements ExportFileApi{

    public boolean export(String data) {

       //简单示意一下

       System.out.println("导出数据"+data+"XML文件");

        return true;

    }

}

然后扩展ExportOperate类,来加入新的实现,示例代码如下:

 

 

/**

 * 扩展ExportOperate对象,加入可以导出XML文件

 */

public class ExportOperate2 extends ExportOperate{

    /**

     * 覆盖父类的工厂方法,创建导出的文件对象的接口对象

     * @param type 用户选择的导出类型

     * @return 导出的文件对象的接口对象

     */

    protected ExportFileApi factoryMethod(int type){

       ExportFileApi api = null;

       //可以全部覆盖,也可以选择自己感兴趣的覆盖,

       //这里只想添加自己新的实现,其它的不管

       if(type==3){

           api = new ExportXml();

       }else{

           //其它的还是让父类来实现

           api = super.factoryMethod(type);

       }

       return api;

    }

}

看看此时的客户端,也非常简单,只是在变换传入的参数,示例代码如下: 

 

public class Client {

    public static void main(String[] args) {

       //创建需要使用的Creator对象

       ExportOperate operate = new ExportOperate2();

       //下面变换传入的参数来测试参数化工厂方法

       operate.export(1,"Test1");

       operate.export(2,"Test2");

       operate.export(3,"Test3");

    }

}

对应的测试结果如下: 

 

导出数据Test1到文本文件

导出数据Test2到数据库备份文件

导出数据Test3XML文件

 

通过上面的示例,好好体会一下参数化工厂方法的实现和带来的好处。


3.5  工厂方法模式的优缺点

  • 可以在不知具体实现的情况下编程
        工厂方法模式可以让你在实现功能的时候,如果需要某个产品对象,只需要使用产品的接口即可,而无需关心具体的实现。选择具体实现的任务延迟到子类去完成。
  • 更容易扩展对象的新版本
        工厂方法给子类提供了一个挂钩,使得扩展新的对象版本变得非常容易。比如上面示例的参数化工厂方法实现中,扩展一个新的导出Xml文件格式的实现,已有的代码都不会改变,只要新加入一个子类来提供新的工厂方法实现,然后在客户端使用这个新的子类即可。
        另外这里提到的挂钩,就是我们经常说的钩子方法(hook),这个会在后面讲模板方法模式的时候详细点说明。
  • 连接平行的类层次
        工厂方法除了创造产品对象外,在连接平行的类层次上也大显身手。这个在前面已经详细讲述了。
  • 具体产品对象和工厂方法的耦合性
        在工厂方法模式里面,工厂方法是需要创建产品对象的,也就是需要选择具体的产品对象,并创建它们的实例,因此具体产品对象和工厂方法是耦合的。

    3.6  思考工厂方法模式

    1:工厂方法模式的本质
            工厂方法模式的本质:延迟到子类来选择实现
            仔细体会前面的示例,你会发现,工厂方法模式中的工厂方法,在真正实现的时候,一般是先选择具体使用哪一个具体的产品实现对象,然后创建这个具体产品对象的示例,然后就可以返回去了。也就是说,工厂方法本身并不会去实现产品接口,具体的产品实现是已经写好了的,工厂方法只要去选择实现就好了。
           有些朋友可能会说,这不是跟简单工厂一样吗?
           确实从本质上讲,它们是非常类似的,具体实现上都是在“选择实现”。但是也存在不同点,简单工厂是直接在工厂类里面进行“选择实现”;而工厂方法会把这个工作延迟到子类来实现,工厂类里面使用工厂方法的地方是依赖于抽象而不是具体的实现,从而使得系统更加灵活,具有更好的可维护性和可扩展性。
           其实如果把工厂模式中的Creator退化一下,只提供工厂方法,而且这些工厂方法还都提供默认的实现,那不就变成了简单工厂了吗?比如把刚才示范参数化工厂方法的例子代码拿过来再简化一下,你就能看出来,写得跟简单工厂是差不多的,示例代码如下:

            看完上述代码,会体会到简单工厂和工厂方法模式是有很大相似性的了吧,从某个角度来讲,可以认为简单工厂就是工厂方法模式的一种特例,因此它们的本质是类似的,也就不足为奇了。

2:对设计原则的体现
        工厂方法模式很好的体现了“依赖倒置原则”。
        依赖倒置原则告诉我们“要依赖抽象,不要依赖于具体类”,简单点说就是:不能让高层组件依赖于低层组件,而且不管高层组件还是低层组件,都应该依赖于抽象。
        比如前面的示例,实现客户端请求操作的ExportOperate就是高层组件;而具体实现数据导出的对象就是低层组件,比如ExportTxtFile、ExportDB;而ExportFileApi接口就相当于是那个抽象。
        对于ExportOperate来说,它不关心具体的实现方式,它只是“面向接口编程”;对于具体的实现来说,它只关心自己“如何实现接口”所要求的功能。
        那么倒置的是什么呢?倒置的是这个接口的“所有权”。事实上,ExportFileApi接口中定义的功能,都是由高层组件ExportOperate来提出的要求,也就是说接口中的功能,是高层组件需要的功能。但是高层组件只是提出要求,并不关心如何实现,而低层组件,就是来真正实现高层组件所要求的接口功能的。因此看起来,低层实现的接口的所有权并不在底层组件手中,而是倒置到高层组件去了。

3:何时选用工厂方法模式
        建议在如下情况中,选用工厂方法模式:

  • 如果一个类需要创建某个接口的对象,但是又不知道具体的实现,这种情况可以选用工厂方法模式,把创建对象的工作延迟到子类去实现
  • 如果一个类本身就希望,由它的子类来创建所需的对象的时候,应该使用工厂方法模式


3.7  相关模式

  • 工厂方法模式和抽象工厂模式
        这两个模式可以组合使用,具体的放到抽象工厂模式中去讲。
  • 工厂方法模式和模板方法模式
        这两个模式外观类似,都是有一个抽象类,然后由子类来提供一些实现,但是工厂方法模式的子类专注的是创建产品对象,而模板方法模式的子类专注的是为固定的算法骨架提供某些步骤的实现。
        这两个模式可以组合使用,通常在模板方法模式里面,使用工厂方法来创建模板方法需要的对象。

 

 

/**

 * 导出成文本文件格式的对象

 */

public class ExportTxtFile implements ExportFileApi{

    public boolean export(String data) {

       //简单示意一下,这里需要操作文件

       System.out.println("导出数据"+data+"到文本文件");

       return true;

    }

}

再看看导出成数据库备份文件形式的对象的实现,示例代码如下:

 

 

/**

 * 导出成数据库备份文件形式的对象

 */

public class ExportDB implements ExportFileApi{

    public boolean export(String data) {

       //简单示意一下,这里需要操作数据库和文件

       System.out.println("导出数据"+data+"到数据库备份文件");

       return true;

    }

}

(3)Creator这边的实现,首先看看ExportOperate的实现,示例代码如下:

 

 

/**

 * 实现导出数据的业务功能对象

 */

public abstract class ExportOperate {

    /**

     * 导出文件

     * @param data 需要保存的数据

     * @return 是否成功导出文件

     */

    public boolean export(String data){

       //使用工厂方法

       ExportFileApi api = factoryMethod();

       return api.export(data);

    }

    /**

     * 工厂方法,创建导出的文件对象的接口对象

     * @return 导出的文件对象的接口对象

     */

    protected abstract ExportFileApi factoryMethod();

}

(4)加入了两个Creator实现,先看看创建导出成文本文件格式的对象,示例代码如下:

 

 

/**

 * 具体的创建器实现对象,实现创建导出成文本文件格式的对象

 */

public class ExportTxtFileOperate extends ExportOperate{

    protected ExportFileApi factoryMethod() {

       //创建导出成文本文件格式的对象

       return new ExportTxtFile();

    }

}

再看看创建导出成数据库备份文件形式的对象,示例代码如下:

 

 

/**

 * 具体的创建器实现对象,实现创建导出成数据库备份文件形式的对象

 */

public class ExportDBOperate extends ExportOperate{

    protected ExportFileApi factoryMethod() {

       //创建导出成数据库备份文件形式的对象

       return new ExportDB();

    }

}

(5)客户端直接创建需要使用的Creator对象,然后调用相应的功能方法,示例代码如下:

 

 

public class Client {

    public static void main(String[] args) {

       //创建需要使用的Creator对象

       ExportOperate operate = new ExportDBOperate();

       //调用输出数据的功能方法

       operate.export("测试数据");

    }

}

运行结果如下:

 

 

导出数据测试数据到数据库备份文件

 

        你还可以修改客户端new的对象,切换成其它的实现对象,试试看会发生什么。看来应用工厂方法模式是很简单的,对吧。

 

3  模式讲解

3.1  认识工厂方法模式

(1)模式的功能
        工厂方法的主要功能是让父类在不知道具体实现的情况下,完成自身的功能调用,而具体的实现延迟到子类来实现。
        这样在设计的时候,不用去考虑具体的实现,需要某个对象,把它通过工厂方法返回就好了,在使用这些对象实现功能的时候还是通过接口来操作,这非常类似于IoC/DI的思想,这个在后面给大家稍详细点介绍一下。


(2)实现成抽象类
        工厂方法的实现中,通常父类会是一个抽象类,里面包含创建所需对象的抽象方法,这些抽象方法就是工厂方法。
        这里要注意一个问题,子类在实现这些抽象方法的时候,通常并不是真的由子类来实现具体的功能,而是在子类的方法里面做选择,选择具体的产品实现对象。
        父类里面,通常会有使用这些产品对象来实现一定的功能的方法,而且这些方法所实现的功能通常都是公共的功能,不管子类选择了何种具体的产品实现,这些方法的功能总是能正确执行。


(3)实现成具体的类
        当然也可以把父类实现成为一个具体的类,这种情况下,通常是在父类中提供获取所需对象的默认实现方法,这样就算没有具体的子类,也能够运行。
        通常这种情况还是需要具体的子类来决定具体要如何创建父类所需要的对象。也把这种情况称为工厂方法为子类提供了挂钩,通过工厂方法,可以让子类对象来覆盖父类的实现,从而提供更好的灵活性。


(4)工厂方法的参数和返回
        工厂方法的实现中,可能需要参数,以便决定到底选用哪一种具体的实现。也就是说通过在抽象方法里面传递参数,在子类实现的时候根据参数进行选择,看看究竟应该创建哪一个具体的实现对象。
        一般工厂方法返回的是被创建对象的接口对象,当然也可以是抽象类或者一个具体的类的实例。


(5)谁来使用工厂方法创建的对象
        这里首先要搞明白一件事情,就是谁在使用工厂方法创建的对象?
        事实上,在工厂方法模式里面,应该是Creator中的其它方法在使用工厂方法创建的对象,虽然也可以把工厂方法创建的对象直接提供给Creator外部使用,但工厂方法模式的本意,是由Creator对象内部的方法来使用工厂方法创建的对象,也就是说,工厂方法一般不提供给Creator外部使用。
        客户端应该是使用Creator对象,或者是使用由Creator创建出来的对象。对于客户端使用Creator对象,这个时候工厂方法创建的对象,是Creator中的某些方法使用。对于使用那些由Creator创建出来的对象,这个时候工厂方法创建的对象,是构成客户端需要的对象的一部分。分别举例来说明。

 

①客户端使用Creator对象的情况
        比如前面的示例,对于“实现导出数据的业务功能对象”的类ExportOperate,它有一个export的方法,在这个方法里面,需要使用具体的“导出的文件对象的接口对象” ExportFileApi,而ExportOperate是不知道具体的ExportFileApi实现的,那么怎么做的呢?就是定义了一个工厂方法,用来返回ExportFileApi的对象,然后export方法会使用这个工厂方法来获取它所需要的对象,然后执行功能。
        这个时候的客户端是怎么做的呢?这个时候客户端主要就是使用这个ExportOperate的实例来完成它想要完成的功能,也就是客户端使用Creator对象的情况,简单描述这种情况下的代码结构如下:

/**

 * 客户端使用Creator对象的情况下,Creator的基本实现结构

 */

public abstract class Creator {

    /**

     * 工厂方法,一般不对外

     * @return 创建的产品对象

     */

    protected abstract Product factoryMethod();

    /**

     * 提供给外部使用的方法,

     * 客户端一般使用Creator提供的这些方法来完成所需要的功能

     */

    public void someOperation(){

       //在这里使用工厂方法

       Product p = factoryMethod();

    }

}

 

②客户端使用由Creator创建出来的对象
        另外一种是由Creator向客户端返回由“工厂方法创建的对象”来构建的对象,这个时候工厂方法创建的对象,是构成客户端需要的对象的一部分。简单描述这种情况下的代码结构如下:

 

②客户端使用由Creator创建出来的对象
        另外一种是由Creator向客户端返回由“工厂方法创建的对象”来构建的对象,这个时候工厂方法创建的对象,是构成客户端需要的对象的一部分。简单描述这种情况下的代码结构如下:

 

 

/**

 * 客户端使用Creator来创建客户端需要的对象的情况下,Creator的基本实现结构

 */

public abstract class Creator {

    /**

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值