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


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

    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中创建对象的方式,可以看做是工厂方法,这些工厂方法的具体实现延迟到子类具体的工厂中去,换句话说,经常会使用工厂方法来实现抽象工厂。

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

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

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

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

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

  • 相关阅读:
    写给初入职场小白的建议
    Python 博客园备份迁移脚本
    x64dbg 2022 最新版编译方法
    LyScript 实现Hook隐藏调试器
    驱动开发:内核中的链表与结构体
    x64dbg 实现插件Socket反向通信
    x64dbg 插件开发SDK环境配置
    C/C++ Capstone 引擎源码编译
    2:手写 instanceof 方法
    3:zindex
  • 原文地址:https://www.cnblogs.com/smarterplanet/p/2712812.html
Copyright © 2020-2023  润新知