首页 > 编程语言 > 详解Java从工厂方法模式到 IOC/DI思想
2021
07-26

详解Java从工厂方法模式到 IOC/DI思想

前言

简单工厂的本质是选择实现,说白了是由一个专门的类去负责生产我们所需要的对象,从而将对象的创建从代码中剥离出来,实现松耦合。我们来看一个例子:

我们要创建一个文件导出工具

public interface FileOper{

  public Boolean exceptFile(String data);
}
public class XMLFileOp implment FileOper{

   public Boolean exceptFile(String data){
      System.out.println("导出一个xml文件");
      return true;
   } 
}
public class Factory{

    public static FileOper createFileOp(){

        return new XMLFileOp();
    } 

}
public Class Test{

 public static void main(String args[]){

    FileOper op = Factory.createFileOp();
    op.exceptFile("测试");

}


}

这样看起来没什么问题,那么我们既然做出来了这个结构,就是为了后续的扩展它,例子中只是为了实现XML文件的导出,后续,我们可以自己实现一个txt文件的导出类,只需要实现FileOper接口就好:

public Class TxtFileOp implment FileOper{     
    public Boolean ExceptFile(String data){      
        System.out.println("导出txt文件");   
        return true; 
    } 
}

这时候我们还是通过工厂来获取这个对象,只需将Factory中追加一个else if 即可通过传参来获取想要的对象了。

工厂方法模式

仔细分析上面的场景,事实上在实现导出文件的业务逻辑中,它根本不知道要使用哪一种导出文件的格式,因此这个对象根本就不应该和具体导出文件的对象耦合在一起,它只需要面向导出文件接口(FileOper)就好,这是工厂的思想,我们上面用简单工厂没错啊,但是后面又加入了新的扩展

这样一来,又有新的问题,面对新的类,简单工厂便不能提供动态的扩展,必须要去修改内部的代码,破坏了开闭原则。我们上一篇也提到了,简单工厂也有它自身的缺陷,其中最严重的就是,它虽然对依赖对象的主体实现了解耦,可是它本身内部却耦合较为严重。这时候我们可以看看工厂方法模式了,工厂方法模式的思路很有意思:老子不管了!采取无为而治的方式。不是需要接口对象么,那就定义一个方法来创建,可是事实上它自己是不知道如何创建这个接口对象的,不过这不重要,定义成抽象方法就行了,交给子类去实现,老子欠债,儿子你来还。

工厂方法的结构

Product:工厂方法所创建的具体对象的统一接口。

Factory:为该类产品的抽象工厂,其内部有声明的工厂方法,工厂方法多为抽象方法,且返回一个Product对象

ProductOne:为具体的产品,也就是Product的具体实现类,真正的工厂产物。

SpecificFactory:具体的工厂,用于生产指定类型的Product,例如图中它只负责生产 ProductOne这个对象。

工厂方法模式的样例代码

public class AbstractFactoryTest {
    public static void main(String[] args) {
        try {
            Product a;
            AbstractFactory af;
            af = (AbstractFactory) ReadXML1.getObject();
            a = af.newProduct();
            a.show();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}
//抽象产品:提供了产品的接口
interface Product {
    public void show();
}
//具体产品1:实现抽象产品中的抽象方法
class ConcreteProduct1 implements Product {
    public void show() {
        System.out.println("具体产品1显示...");
    }
}
//具体产品2:实现抽象产品中的抽象方法
class ConcreteProduct2 implements Product {
    public void show() {
        System.out.println("具体产品2显示...");
    }
}
//抽象工厂:提供了厂品的生成方法
interface AbstractFactory {
    public Product newProduct();
}
//具体工厂1:实现了厂品的生成方法
class ConcreteFactory1 implements AbstractFactory {
    public Product newProduct() {
        System.out.println("具体工厂1生成-->具体产品1...");
        return new ConcreteProduct1();
    }
}
//具体工厂2:实现了厂品的生成方法
class ConcreteFactory2 implements AbstractFactory {
    public Product newProduct() {
        System.out.println("具体工厂2生成-->具体产品2...");
        return new ConcreteProduct2();
    }
}

基于XML解析的外部配置文件

class ReadXML1 {
    //该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象
    public static Object getObject() {
        try {
            //创建文档对象
            DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = dFactory.newDocumentBuilder();
            Document doc;
            doc = builder.parse(new File("src/FactoryMethod/config1.xml"));
            //获取包含类名的文本节点
            NodeList nl = doc.getElementsByTagName("className");
            Node classNode = nl.item(0).getFirstChild();
            String cName = "FactoryMethod." + classNode.getNodeValue();
            //System.out.println("新类名:"+cName);
            //通过类名生成实例对象并将其返回
            Class<?> c = Class.forName(cName);
            Object obj = c.newInstance();
            return obj;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

上面是一个较为初级也较为经典的工厂方法模板,工厂方法还有另一种用法,即抽象的工厂父类除了创建对象的方法之外,还包含其他的一些方法,而工厂父类通常使用这些方法来完成某些任务,下面我们来看看这第二种表现方式。

工厂方法模式实现文件导出

根据上面工厂方法模式提供的思路,我们重新来思考并实现一下一开始那个文件导出的功能。

/**
 * 文件导出接口
 * 实现将指定数据的导出
 * 扩展:实现该接口,可指定生产具体文件类型
 * @author GCC
 */
public interface ExportFileApi {

    /**
     * 导出文件
     * @param data 待导出数据
     * @return boolean
     */
    boolean exportFile(String data);

}
/**
 * 生产Excel文件导出器
 * @author GCC
 */
public class ExportExcelFile implements ExportFileApi {
    @Override
    public boolean exportFile(String data) {
        //todo 处理数据
        return false;
    }
}
/**
 * 导出功能口,工厂
 * @author GCC
 */
public abstract class ExportFileOperate {

    public Logger logger = Logger.getLogger(ExportFileOperate.class);
    //使用产品对象来实现一定功能的方法,这里是实现数据导出
    public void export(String data){
        ExportFileApi exportoper = methodFactory();
        if(exportoper.exportFile(data)){
            logger.info("文件导出成功");
            return;
        }
        logger.error("文件导出失败");

    }

    //工厂方法
    protected abstract ExportFileApi;

}
/**
 * 将指定数据导出为Excel文件
 * @author GCC
 */
public class ExportExcelFileFactory extends ExportFileOperate {

    @Override
    protected ExportFileApi methodFactory() {
        return new ExportExcelFile();
    }
}
/**
 * 客户用例
 */
public class App 
{
    static Logger logger = Logger.getLogger(App.class);

    public static void main( String[] args )
    {
        ExportFileOperate ex = new ExportExcelFileFactory();
        ex.export("测试数据");
    }
}

这里大家可能会 有疑惑,你这个实现方式怎么和前面提到的工厂方法模式的标准样例不一样? 其实这是工厂方法模式的另一种结构,确切地说这才是真正意义上的工厂方法模式(上面的模板只是工厂方法的正常形态)。

这一种的实现方式是 :父类会是一个抽象类,里面包含创建所需对象的抽象方法(代码样例中ExportFileOperat类的methodFactory()方法,这里ExportFileOperat类就是所谓的工厂类,需要补充的是设计模型的使用,不要拘泥于命名名称,可以根据实际需求来进行相应的变化),这些抽象方法就是工厂方法模式中的工厂方法。父类里面,通常会有使用这些产品对象来实现一定的功能的方法(代码样例中ExportFileOperat类的export()方法)。而这些方法所实现的功能通常都是公共功能,不管子类选择了何种具体的产品实现,这些方法总能正常运行。

之所以会有上面两种方式,主要原因在于工厂方法对于客户端的支持,这里需要弄清楚一个问题,谁在使用工厂方法所创建的对象?

事实上,在工厂方法模式里,应该是工厂中的其他方法来使用工厂所创建的对象,为了方便,工厂方法创建的对象也可直接提供给外部的客户端来调用,但工厂方法的本意是由Factory抽象父类内部的方法来使用工厂方法创建的对象。

以下这幅时序图,即说明了客户端调用factory的两种方式。

其实客户端应该使用Factory对象,或者是由Factory所创建出来的产品对象,对于客户端使用Factory对象,这个时候工厂方法创建的对象,是Factory中的某些方法在用,对于使用那些由Factory创建出来的对象,这个时候工厂方法创建的对象,是构成客户端所需对象的一部分。

工厂方法与简单工厂的区别

下面我们看一个例子,这里我打算做个计算器,如果用简单工厂模式来做,它的结构是这样的:

public class SimpleFactory {
    public Calculator create(String type){

        switch (type){
            case "+":
                //返回一个具有加法功能的计算器对象
                return new AddCalculator();
            case "-":
                //返回一个具有减法功能的计算器对象
                return new DeCalculator();
            default:return null;
        }
    }
}

为了工厂更完整,采用传参的静态工厂方式来实现,这样我简单工厂里将通过Switch语句来管控生产哪一种计算类,这时候,突然来了新的需求,我需要一个乘法的功能,这时候我就得实现计算器接口,完成一个乘法类,同时去简单工厂的代码里,追加一个case。

同理,我使用工厂方法的模式来做这个功能,这块的类图则如上图一样,我的工厂代码里不需要Switch了,只需要一个具有生产计算器对象的抽象方法的抽象工厂类即可,当我需要一个乘法能力的计算器时,实现计算器接口,完成乘法类,然后继承抽象工厂,完成一个乘法的工厂子类,然后再用乘法的工厂子类来创建乘法类。然后再去修改客户端。

上面一对比,嘿,这升级版的工厂方法怎么比简单工厂还复杂了!?肯定很多同学在看工厂设计模式的时候很困惑,简单工厂和工厂方法的区别在哪,明明感觉用简单工厂更方便呢?

其实回头好好看看设计原则,就会发现,这是一个解耦的过程,简单工厂模式最大的优点在于工厂类中包含了必要的逻辑判断,根据客户的选择动态的实例化相关的产品类,对于客户端来说,除去了与具体产品对象的依赖。但问题就是随着你的新需求,如果使用简单工厂,那么你就不得不去破坏开闭原则,而看起来改动更为复杂的工厂方法模式,你并不需要对以前的代码进行改动,只需要继承,扩展即可。仔细观察一下,简单工厂是让客户端与依赖对象进行解耦,而工厂方式模式又是对工厂的一层解耦,原本内部耦合性较强的if else,变成了由客户端或者配置文件来控制,工厂方法将简单工厂内部的逻辑判断移到了使用它的外部(客户端或者配置文件)来控制。本来扩展是需要修改工厂类源代码的,现在变成了客户端修改调用或者配置文件中的一个参数。

工厂方法模式的意义

工厂方法模式的主要思想是让父类在不知情的情况下,完成自身功能的调用,而具体的实现则延迟到子类来做;或者说是在静态工厂中,将其原本耦合的if else抽离出来,配合配置文档使用,将写死的if else灵活实现(配置文件并不是默认必须要有的)。这样在设计的时候,不用去考虑具体的实现,需要某个对象,把它通过工厂方法返回就好,在使用这些对象实现功能的时候还是通过接口来操作,这里就有一点IOC的韵味了。

工厂方法模式与IOC、DI

什么是IOC/DI?

想想之前没有学习设计模式,刚学会使用Java就被Spring的bean配置文件支配的恐惧。

那时候你说自己学Java,对方一定会问你Spring,说到Spring,肯定避不开“什么是IOC,什么是DI?”这个让人头痛的问题,那么,到底什么是IOC,DI?

看完工厂的设计思想,对这个问题才开始了真正的思考。

IOC——控制反转

DI——依赖注入

除了上面脱口而出的回答,我想我们这些面向对象的程序猿们,应该有个更深入的理解,到底什么是控制反转,什么是依赖注入。要想理解上面两个概念,必须把问题拆开来看,先搞清楚基本的问题几个问题:

主客体是谁,或者说参与这个概念的都有谁?

什么叫依赖?为什么会有依赖?

什么叫注入?注入的是什么?谁注入谁?

控制反转,谁控制谁,控制的是什么,既然叫反转,正转是啥?

下面我们一个个来解决问题:

1、参与者,说起参与者,一般我们在这个概念中是有三个参与者,具体某个类,容器,某个对象所依赖的外部资源(另一个对象),就好比我有三个类,A,B,C,A对象我们把它想象成一个客户端,B是它需要的一个外部的资源,C是一个叫容器的第三方。

2、什么叫依赖,这个就比较好说了,你有一个A类,但是你A类的成员变量有一个是B类的实例声明,那么A就依赖于B,也就是说,A如果想正常运转(或者说功能正常),必须得依赖于它的成员变量B,至于为什么会有依赖,那也好理解了,面向对象就是将功能封装,每个对象都功能单一,这样有些复杂的对象需要实现复杂的功能,就必须需要其他类的协同。

3、注入,就是说,A你的成员变量B只是声明了一个变量,它对于对象A来说,只是一个引用,一个字符,本身并没有实体,你可以new 一下这个变量的构造函数,才能使这个变量真正有灵魂,又或者用它来承接外部传进来的同类实体,这里外部传进来B的方式就叫注入,注入的是这个变量类型 具体的实例化对象。谁来注入,当然是容器C来注入给A,将B注入给A

4、简单来说就是容器来控制A,控制的就是A所依赖的对象B实例的创建,反转是与正转对应来说的,什么是正转呢,A类中有个对象B的成员变量,正常情况下,A类中功能用到B对象的时候,A要主动去获取一个B对象,例如new一下,这种情况被称为正向的。这样就比较好理解反转了,A不再去主动获取B对象了,而是被动的等待B的到来(注入),等待容器C获取一个B的实例,然后反向的注入进A。

所以,综上来看,控制反转和依赖注入其实说的是同一件事,说白了就是对象创建这个责任归谁的问题,依赖注入是从应用程序的角度去描述,应用程序依赖外部容器去创建并注入它所需要的外部资源对象。控制反转是从容器的角度出发,容器控制应用程序,由容器反向地向应用程序注入其所需要的外部对象。

其实IOC/DI并不是一种代码实现,更多的它是一种思想,它从思想上完成了 “主从换位” 的变化,应用程序本来是主体,占绝对地位,它需要什么都会主动出击去获取,过强的控制欲导致了它耦合过重,而在IOC/DI思想中,应用程序变成被动的等待容器的注入,需要啥只能提出来,什么时候给,给什么样子的,主动权完全交给了容器,较强的实现了解耦,程序的灵活性也就高了

工厂方法与IOC/DI思想

从某个角度来看,工厂方法模式跟IOC/DI的思想很贴近。

上面也说过了,IOC/DI就是让应用程序不再主动获取外部资源,而是被动等待第三方的注入,那么在编写程序的时候,一旦遇到需要外部资源的地方,就会开一个窗口,提供给容器一个注入的途径,让容器注入进来,细节这里就不过多赘述了,自己去找Spring聊吧。 下面用IOC/DI和工厂方法来实现一个样例对比一下。

用IOC/DI来实现一个类Person:

public class Person {

    private String name;
    //依赖文件操作对象
    private FileUtil fileUtil;

    private int age;

    //提供set方法,供外部注入
    public void setFileUtil(FileUtil fileUtil){
        this.fileUtil = fileUtil;
    }
    
    public void opFile(String fileurl){
        fileUtil.createFile(fileurl);
    }
    

}

这就是IOC/DI思想来实现的一个类,我依赖FileUtil,没事,我不管,我提供给你一个set的注入途径,剩下的我不管了,我就默认我用FileUtil的时候,它是真真切切存在在堆中的对象。(本质是当外部的容器,创建Person对象的时候,会发现它依赖FileUtil,然后容器去获取一个FileUtil,通过Person提供的Set方法,将获取的FileUtil对象注入进去,然后一个完整的Person对象就被制造出来了,这个过程Person角度来看,Person是无感的)

下面用工厂方法来搞上面的例子:

public abstract class Person {

    private String name;

    private int age;

    //交给子类去实现我的依赖
    public abstract FileUtil getFileutil();

    public void opFile(String fileurl){
        getFileutil().createFile(fileurl);
    }


}

这里,Person类也是需要用到FileUtil,但是它也不需要主动的去new一下FileUtil,而是通过抽象方法的形式,将FileUtil的实例化延申到子类去实现,其实就是变相地提供了一种注入渠道(标准bean中是通过set方法,容器调用Set方法注入需要的对象,而工厂方法则是通过实现子类,即让子类来完成依赖对象的注入)

仔细体会这两种写法,对比他们的实现,在思想层面来看,会发现,工厂方法模式和IOC/DI的思想是相似的,都是“主动变被动”,“主位换从位”,从而获得了更加灵活的程序结构。

以上就是详解Java从工厂方法模式到 IOC/DI思想的详细内容,更多关于Java从工厂方法模式到 IOC/DI思想的资料请关注自学编程网其它相关文章!

编程技巧