工厂方法模式

1 实际场景

1.1 导出数据的应用框架

假设某个企业应用,需要每天定期额汇总各个分公司的业务数据,进行数据导入与核算。但是各个公司分布在不同的地方,且公司没有建设专用内网的条件。 需要各个公司将不同数据导出,然后由专门运输渠道将数据上交给总部。

则此应用需要:支持多种数据格式的导出(如:文本、数据库备份、Excel 格式、Xml 格式等等),并真正执行数据导出动作。

1.2 存在的问题

分析上面的需求,不管用户选择何种导出格式,最后导出的都是一个文件,而且系统本身不能感知最终要导出成何种文件,因此应该有一个统一的接口,描述系统最后生成的对象,并操作输出的文件。

到处文件对象的接口定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @Auther mark
* @Version ExprotFileApi.java v1.0 Exp $
* @Description 导出的文件对象的接口
*/
public interface ExportFileApi {

/**
* 导出的文件对象的接口
*
* @param data 示意:需要保存的数据
* @return 是否导出成功
*/
public boolean export(String data);
}

对于实现导出数据的业务功能对象,它应该根据需要来创建相应的 ExportFileApi 的实现对象,因为特定的 ExportFileApi 的实现是与具体的业务相关的。但是对于实现导出数据的业务功能对象而言,它并不知道应该创建哪一个 ExportFileApi 的实现对象,也不知道如何创建。

即:对于实现导出数据的业务功能对象,需要创建 ExportFileApi 的具体实例对象,但是它只能感知该接口,而不知道起具体的实现。

2 解决方案

2.1 工厂方法模式

2.1.1 定义

定义一个用于创建对象的接口,让子类决定实例化哪一个类,工厂方法使一个类的实例化延迟到其子类。

2.1.2 工厂方法模式解决问题思路

通过分析上面的问题,事实上在实现导出数据的业务功能对象里面,根本就不知道究竟要使用哪一种导出文件的格式,因此这个对象本就不应该和具体的导出文件的对象耦合在一起,它只需要面向导出的文件对象的接口就好了

但是这样一来,又有新的问题产生了:接口是不能直接使用的,需要使用具体的接口实现对象的实例

这不是自相矛盾吗?要求面向接口,不让和具体的实现耦合,但是又需要创建接口的具体实现对象的实例。怎么解决这个矛盾呢?

工厂方法模式的解决思路很有意思,那就是不解决,采取无为而治的方式:不是需要接口对象吗,那就定义一个方法来创建;可是事实上它自己是不知道如何创建这个接口对象的,没有关系,那就定义成抽象方法就好了,自己实现不了,那就让子类来实现,这样这个对象本身就可以只是面向接口编程,而无需关心到底如何创建接口对象了

2.2 结构说明

结构图

image-20221225210314824
  • Product:定义工厂方法所创建的对象的接口,即实际需要使用的对象接口;
  • ConcreteProduct:继承与 Product 接口的具体实现对象;
  • Creator:创建器,声明工厂方法,工厂方法通常会返回一个 Product 类型的实例对象,而且多是抽象方法。也可以在 Creator 里面提供工厂发的默认实现,让工厂方法返回一个缺省的实例对象;
  • ConcreteCreator:具体的创建器对象,覆盖实现的 Creator 定义的工厂方法,返回具体的 Product 实例对象;

2.3 代码示例

Product.java

1
2
3
4
5
6
7
8
/**
* @Auther mark
* @Version Product.java v1.0 Exp $
* @Description 工厂方法所创建的对象的接口
*/
public interface Product {
// 可以定义Product的属性和方法
}

ConcreteProduct.java

1
2
3
4
5
6
7
8
9
/**
* @Auther mark
* @Version ConcreteProduct.java v1.0 Exp $
* @Description 具体的Product对象
*/
public class ConcreteProduct implements Product {
// 实现Product要求的方法
}

Creator.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @Auther mark
* @Version Creator.java v1.0 Exp $
* @Description 创建器,声明工厂方法
*/
public abstract class Creator {

/**
* 示意方法,实现某些功能的方法
*/
public void someOperation() {
Product product = factoryMethod();
}

/**
* 创建Product的工厂方法
*
* @return Product对象
*/
protected abstract Product factoryMethod();
}

ConcreteCreator.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @Auther mark
* @Version ConcreteCreator.java v1.0 Exp $
* @Description 具体的创建器实现对象
*/
public class ConcreteCreator extends Creator {

@Override
protected Product factoryMethod() {
// 重定义工厂方法,返回一个具体的Product对象
return new ConcreteProduct();
}

}

2.4 重写方案

要使用工厂方法模式来实现示例,先来按照工厂方法模式的结构,对应出哪些是被创建的 Product,哪些是 Creator。分析要求实现的功能,导出的文件对象接口 ExportFileApi 就相当于是 Product,而用来实现导出数据的业务功能对象就相当于 Creator

代码结构图:

image-20221225214636906
  1. 导出文件的对象接口 ExportFileApi 实现同上

  2. 接口 ExportFileApi 的实现,为了示例简单,只实现导出文本文件格式和数据库备份文件两种

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    /**
    * @Auther mark
    * @Version ExportTxtFile.java v1.0 Exp $
    * @Description 导出成文文件格式的对象
    */
    public class ExportTxtFile implements ExportFileApi {
    @Override
    public boolean export(String data) {
    // 示意代码,操作文件
    System.out.println("导出数据" + data + "到文本文件");
    return true;
    }
    }

    /**
    * @Auther mark
    * @Version ExportDB.java v1.0 Exp $
    * @Description 导出成数据库备份文件形式的对象
    */
    public class ExportDB implements ExportFileApi {
    @Override
    public boolean export(String data) {
    // 示意代码,操作数据库和文件
    System.out.println("导出数据" + data + "到数据库备份文件");
    return true;
    }
    }
  3. 工厂模版 ExportOperate 的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    /**
    * @Auther mark
    * @Version ExportOperate.java v1.0 Exp $
    * @Description 实现导出数据的业务功能对象
    */
    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 的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    /**
    * @Auther mark
    * @Version ExportTxtFileOperate.java v1.0 Exp $
    * @Description 具体的创建器实现对象,实现创建导出成文本文件格式的对象
    */
    public class ExportTxtFileOperate extends ExportOperate {
    @Override
    protected ExportFileApi factoryMethod() {
    //创建导出成文本文件格式的对象
    return new ExportTxtFile();
    }
    }

    /**
    * @Auther mark
    * @Version ExportDBOperate.java v1.0 Exp $
    * @Description 具体的创建器实现对象,实现创建导出成数据库备份文件形式的对象
    */
    public class ExportDBOperate extends ExportOperate {
    @Override
    protected ExportFileApi factoryMethod() {
    //创建导出成数据库备份文件形式的对象
    return new ExportFileApi();
    }
    }
  5. 客户端使用 Creator 对象,使用相应功能的示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * @Auther mark
    * @Version Client.java v1.0 Exp $
    * @Description
    */
    public class Client {
    public static void main(String[] args) {
    //创建需要使用的Creator对象
    ExportOperate operate = new ExportDBOperate();
    // 调用输出数据的方法
    operate.export("测试数据");
    }
    }

3 模式讲解

3.1 认识工厂方法模式

3.1.1 功能

工厂方法的主要功能是让父类在不知道具体实现的情况下,完成自身的功能调用,而具体的实现延迟到子类来实现。

这样在设计的时候,不用去考虑具体的实现,需要某个对象,把它通过工厂方法返回就好了,在使用这些对象实现功能的时候还是通过接口来操作,这非常类似于 IoC/DI 的思想(后面介绍)。

3.1.2 实现成抽象类

工厂方法的实现中,通常父类会是一个抽象类,里面包含创建所需对象的抽象方法,这些抽象方法就是工厂方法。

注意:子类在实现这些抽象方法的时候,通常并不是真的由子类来实现具体的功能,而是在子类的方法里面做选择,选择具体的产品实现对象

父类里面,通常会有使用这些产品对象来实现一定的功能的方法,而且这些方法所实现的功能通常都是公共的功能,不管子类选择了何种具体的产品实现,这些方法的功能总是能正确执行。

3.1.3 实现成具体的类

父类也可以实现成为一个具体的类,这种情况下,通常是在父类中提供获取所需对象的默认实现方法,这样就算没有具体的子类,也能够运行

通常这种情况还是需要具体的子类来决定具体要如何创建父类所需要的对象。也把这种情况称为工厂方法为子类提供了挂钩,通过工厂方法,可以让子类对象来覆盖父类的实现,从而提供更好的灵活性。

3.1.4 工厂方法的参数和返回

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

一般工厂方法返回的是被创建对象的接口对象,当然也可以是抽象类或者一个具体的类的实例。

3.1.5 谁使用工厂方法创建的对象

事实上,在工厂方法模式里面,应该是 Creator 中的其它方法在使用工厂方法创建的对象,虽然也可以把工厂方法创建的对象直接提供给 Creator 外部使用,但工厂方法模式的本意,是由 Creator 对象内部的方法来使用工厂方法创建的对象,也就是说,工厂方法一般不提供给 Creator 外部使用

客户端应该是使用 Creator 对象,或者是使用由 Creator 创建出来的对象。

  • 对于客户端使用 Creator 对象,这个时候工厂方法创建的对象,是 Creator 中的某些方法使用。
  • 对于使用由 Creator 创建出来的对象,这个时候工厂方法创建的对象,是构成客户端需要的对象的一部分。

分别举例来说明。

  1. 客户端使用 Creator 对象

    比如前面的示例,对于 “实现导出数据的业务功能对象” 的类 ExportOperate,在 export 方法里面,需要使用具体的 “导出的文件对象的接口对象” ExportFileApi,而 ExportOperate 是不知道具体的 ExportFileApi 实现的,那么怎么做的呢?就是定义了一个工厂方法,用来返回 ExportFileApi 的对象,然后 export 方法会使用这个工厂方法来获取它所需要的对象,然后执行功能

    这个时候客户端主要就是使用这个 ExportOperate 的实例来完成它想要完成的功能,也就是客户端使用 Creator 对象的情况,简单描述这种情况下的代码结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    * @Auther mark
    * @Version Creator.java v1.0 Exp $
    * @Description 创建器,声明工厂方法
    */
    public abstract class Creator {

    /**
    * 示意方法,实现某些功能的方法
    */
    public void someOperation() {
    Product product = factoryMethod();
    }

    /**
    * 创建Product的工厂方法
    *
    * @return Product对象
    */
    protected abstract Product factoryMethod();
    }
  2. 客户端使用由 Creator 创建出来的对象

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    /**
    * 客户端使用Creator来创建客户端需要的对象的情况下,Creator的基本实现结构
    */
    public abstract class Creator {
    /**
    * 工厂方法,一般不对外,创建一个部件对象
    * @return 创建的产品对象,一般是另一个产品对象的部件
    */
    protected abstract Product1 factoryMethod1();
    /**
    * 工厂方法,一般不对外,创建一个部件对象
    * @return 创建的产品对象,一般是另一个产品对象的部件
    */
    protected abstract Product2 factoryMethod2();
    /**
    * 创建客户端需要的对象,客户端主要使用产品对象来完成所需要的功能
    * @return 客户端需要的对象
    */
    public Product createProduct(){
    //在这里使用工厂方法,得到客户端所需对象的部件对象
    Product1 p1 = factoryMethod1();
    Product2 p2 = factoryMethod2();
    //工厂方法创建的对象是创建客户端对象所需要的
    Product p = new ConcreteProduct();
    p.setProduct1(p1);
    p.setProduct2(p2);

    return p;
    }
    }

小结:在工厂方法模式里面,客户端要么使用 Creator 对象,要么使用 Creator 创建的对象,一般客户端不直接使用工厂方法。当然也可以直接把工厂方法暴露给客户端操作,但是一般不这么做。

3.2 工厂方法模式与 IoC/DI

IoC——Inversion of Control 控制反转

DI——Dependency Injection 依赖注入

3.2.1 理解 IoC/DI

几个问题:

  1. 参与者都有谁?
  2. 依赖:谁依赖于谁?为什么需要依赖?
  3. 注入:谁注入谁?注入什么?
  4. 控制反转:谁控制谁?控制什么?为什么是反转?
  5. 依赖注入和控制反转的区别?

回答:

  1. 参与者

    一般有三方参与者,一个是某个对象;一个是 IoC/DI 的容器;另一个是某个对象的外部资源

    • 某个对象指的就是任意的、普通的 Java 对象;
    • IoC/DI 的容器简单点说就是指用来实现 IoC/DI 功能的一个框架程序;
    • 对象的外部资源指的就是对象需要的,但是是从对象外部获取的,都统称资源,比如:对象需要的其它对象、或者是对象需要的文件资源等等。
  2. 谁依赖谁

    某个对象依赖于 IoC/DI 的容器

  3. 为什么需要依赖

    对象需要 IoC/DI 的容器来提供对象需要的外部资源

  4. 谁注入谁

    IoC/DI 容器注入对象

  5. 注入什么

    IoC/DI 容器注入对象需要的外部资源

  6. 谁控制谁

    IoC/DI 容器控制对象

  7. 控制什么

    控制对象实例的创建

  8. 反转是什么

    假设在应用程序中,如果需要在 A 里面使用 C,一般是直接去创建 C 的对象实例,然后在 A 类中主动去获取所需要的外部资源 C,这种称为正向。反向则是在 A 类中不再主动去获取 C,而是被动等待,等待 IoC/DI 容器获取一个 C 的实例,然后反向注入到 A 中。

    正向:

    image-20230101232319654

    反向:

    image-20230101232426759

  9. 依赖注入与控制反转

    是对同一件事情的不同描述。

    依赖注入从应用程序的角度描述,应用程序依赖容器创建并注入它所需要的外部资源

    控制反转从容器的角度描述,容器控制应用程序,有容器反向的向应用程序注入应用程序所需要的外部资源

3.2.2 对比工厂模式与 IoC/DI

IoC/DI 代码示例:

资源 C:

1
2
3
public interface C {
void func();
}

使用者 A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class A {

/**
* 被注入的资源
*/
private C faze = null;

/**
* 注入资源C的方法
* @param faze 被注入的资源
*/
public void setFaze(C faze) {
this.faze = faze;
}

public void fun() {
/**
* 外部注入需要使用的资源,不需要主动创建
*/
faze.func();
}
}

从上面的示例代码可以看出,现在在 A 里面写代码的时候,凡是碰到了需要外部资源,那么就提供注入的途径,要求从外部注入,自己只管使用这些对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class A1 {

/**
* 工厂方法,创建资源,从子类注入进来的途径
*
* @return 资源的对象实例
*/
protected abstract C createC();

public void t1() {
/**
* 需要使用资源C,但是不主动创建,等待注入
*/
createC().func();
}
}

子类的示例代码如下:

1
2
3
4
5
6
public class A2 extends A1 {
protected C1 createC1() {
//真正的选择具体实现,并创建对象
return new C2();
}
}

总结:从思想层面上,会发现工厂方法模式和 IoC/DI 的思想是相似的,都是 “主动变被动”,进行了 “主从换位”,从而获得了更灵活的程序结构

3.3 工厂方法模式的优缺点

1 可以在不知具体实现的情况下编程

工厂方法模式可以在实现功能的时候,如果需要某个产品对象,只需要使用产品的接口即可,而无需关心具体的实现。选择具体实现的任务延迟到子类去完成。

更容易扩展对象的新版本。工厂方法给子类提供了一个挂钩,使得扩展新的对象版本变得非常容易。

2 具体产品对象和工厂方法的耦合性

在工厂方法模式里面,工厂方法是需要创建产品对象的,也就是需要选择具体的产品对象,并创建它们的实例,因此具体产品对象和工厂方法是耦合的。

3.4 思考工厂方法模式

3.4.1 工厂方法模式的本质

本质:延迟到子类来选择实现

工厂方法模式中的工厂方法,在真正实现的时候,一般是先选择具体使用哪一个具体的产品实现对象,然后创建这个具体产品对象的示例,然后返回。也就是说,工厂方法本身并不会去实现产品接口,具体的产品实现是已经写好了,工厂方法只要去选择实现。

从本质上讲,工厂方法模式和简单工厂模式非常类似,具体实现上都是在 “选择实现”。但是也存在不同点,简单工厂是直接在工厂类里面进行 “选择实现”;而工厂方法会把这个工作延迟到子类来实现,工厂类里面使用工厂方法的地方是依赖于抽象而不是具体的实现,从而使得系统更加灵活,具有更好的可维护性和可扩展性。

如下,工厂方法模式 Creator 退化,只提供工厂方法,且提供默认的实现,这就是简单工厂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ExportOperate {

/**
* 导出文件
*
* @param data 需要保存的数据
* @return 是否成功导出文件
*/
public boolean export(String data) {
// 使用工厂方法
ExportFileApi api = factoryMethod();
return api.export(data);
}

/**
* 工厂方法,创建导出的文件对象的接口对象
*
* @return 导出的文件对象的接口对象
*/
protected ExportFileApi factoryMethod(int type) {
ExportFileApi api = null;
// 根据类型选择究竟要创建哪种到处文件对象
if (type == 1) {
api = new ExportTxtFile();
} else if (type == 2) {
api = new ExportDB();
}
return api;
}
}

3.4.2 对设计原则的体现

依赖倒置原则告诉我们 “要依赖抽象,不要依赖于具体类”,简单点说就是:不能让高层组件依赖于低层组件,而且不管高层组件还是低层组件,都应该依赖于抽象

如前面的示例,实现客户端请求操作的 ExportOperate 就是高层组件;而具体实现数据导出的对象就是低层组件,比如 ExportTxtFile、ExportDB;而 ExportFileApi 接口就相当于是那个抽象。

对于 ExportOperate 来说,它不关心具体的实现方式,它只是 “面向接口编程”;对于具体的实现来说,只关心自己 “如何实现接口” 所要求的功能。

那么倒置的是什么呢?倒置的是这个接口的 “所有权”。事实上,ExportFileApi 接口中定义的功能,都是由高层组件 ExportOperate 来提出的要求,也就是说接口中的功能,是高层组件需要的功能。但是高层组件只是提出要求,并不关心如何实现,而低层组件,就是来真正实现高层组件所要求的接口功能的。因此看起来,低层实现的接口的所有权并不在底层组件手中,而是倒置到高层组件去了

3.4.3 何时选用工厂方法模式

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

3.5 相关模式

3.5.1 工厂方法模式和抽象工厂模式

组合使用 - 抽象工厂模式

3.5.2 工厂方法模式和模版方法模式

这两个模式外观类似,都是有一个抽象类,然后由子类来提供一些实现,但是工厂方法模式的子类专注的是创建产品对象,而模板方法模式的子类专注的是为固定的算法骨架提供某些步骤的实现

这两个模式可以组合使用,通常在模板方法模式里面,使用工厂方法来创建模板方法需要的对象。