Skip to main content

工厂方法模式 (Factory Method)

场景问题​

问题案例​

还是用“生成器模式”中的数据导出为文件的例子,我们需要根据不同的数据源生成不同的文件格式,如生成Excel文件、CSV文件等。要导出的文件类型有很多,我们就需要一个统一的应用框架,来实现接入不同的目标文件,否则我们就要将各种文件转换的代码硬编码到业务代码中,根据文件扩展名来判断走哪一段导出逻辑,这样的代码不仅难以维护,而且不符合开闭原则。

而要实现这样的框架,首先与业务代码对接部分的导出能力接口,就必须是保持不变的,这样才能保证业务代码不受影响。其次,我们需要一个工厂类,根据不同的文件类型,生成不同的导出器,这样就可以实现不同的导出逻辑,而不需要修改业务代码。这样无论日后增加多少种导出文件类型,都可以通过增加新的导出器来实现,而不需要修改业务代码。业务代码仅根据用户选择的文件格式,调用工厂方法生成对应的导出器,然后调用导出器的导出方法即可。

不用模式的解决方案​

在不使用工厂方法模式的情况下,我们需要在业务代码中根据文件扩展名来判断走哪一段导出逻辑,这样的代码不仅难以维护,而且不符合开闭原则。

public class ExportData {
public void export(String data, String fileType) {
if ("excel".equals(fileType)) {
// 导出Excel文件
} else if ("csv".equals(fileType)) {
// 导出CSV文件
} else if ("pdf".equals(fileType)) {
// 导出PDF文件
}
// ...
}
}

有何问题​

  1. 业务代码与导出器的实现耦合,导致业务代码难以维护。
  2. 业务代码需要根据文件扩展名来判断走哪一段导出逻辑,不符合开闭原则。
  3. 业务代码需要知道导出器的实现细节,导致业务代码不稳定。
  4. 业务代码需要知道导出器的实现细节,导致业务代码不可扩展。
  5. 业务代码需要知道导出器的实现细节,导致业务代码不可复用。

解决方案​

工厂方法模式的定义​

用来解决上述问题的一个合理的解决方案就是工厂方法模式(Factory Method)。那么什么是工厂方法模式呢?

  1. 工厂方法模式的定义Factory Method

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

  2. 应用工厂方法模式来解决问题的思路

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

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

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

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

工厂方法模式的结构​

design-fm.svg

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

工厂方法模式的实现​

/**
* 工厂方法所创建的对象的接口
*/
public interface Product {
//可以定义Product的方法
}

/**
* 具体的Product对象
*/
public class ConcreteProduct implements Product {
//实现Product要求的方法
}

/**
* 创建器,声明工厂方法
*/
public abstract class Creator {
/**
* 创建Product的工厂方法
* @return Product对象
*/
protected abstract Product factoryMethod();
/**
* 示意方法,实现某些功能的方法
*/
public void someOperation() {
//通常在这些方法实现中,需要调用工厂方法来获取Product对象
Product product = factoryMethod();
}
}

/**
* 具体的创建器实现对象
*/
public class ConcreteCreator extends Creator {
public Product factoryMethod() {
//重定义工厂方法,返回一个具体的Product对象
return new ConcreteProduct();
}
}

工厂方法重写示例​

示例结构​

design-fm-1.svg

示例代码​

public class Client {
public static void main(String[] args) {
//创建需要使用的Creator对象
ExportOperate operate = new ExportDBOperate();
//调用输出数据的功能方法
operate.export("测试数据");
}
}

/**
* 实现导出数据的业务功能对象
*/
public abstract class ExportOperate {
/**
* 导出文件
* @param data 需要保存的数据
* @return 是否成功导出文件
*/
public boolean export(String data){
//使用工厂方法
ExportFileApi api = factoryMethod();
return api.export(data);
}
/**
* 工厂方法,创建导出的文件对象的接口对象
* @return 导出的文件对象的接口对象
*/
protected abstract ExportFileApi factoryMethod();
}

/**
* 具体的创建器实现对象,实现创建导出成数据库备份文件形式的对象
*/
public class ExportDBOperate extends ExportOperate{
protected ExportFileApi factoryMethod() {
//创建导出成数据库备份文件形式的对象
return new ExportDB();
}
}

/**
* 具体的创建器实现对象,实现创建导出成文本文件格式的对象
*/
public class ExportTxtFileOperate extends ExportOperate{

protected ExportFileApi factoryMethod() {
//创建导出成文本文件格式的对象
return new ExportTxtFile();
}

}

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

/**
* 导出成数据库备份文件形式的对象
*/
public class ExportDB implements ExportFileApi{
public boolean export(String data) {
//简单示意一下,这里需要操作数据库和文件
System.out.println("导出数据"+data+"到数据库备份文件");
return true;
}
}

/**
* 导出成文本文件格式的对象
*/
public class ExportTxtFile implements ExportFileApi{
public boolean export(String data) {
//简单示意一下,这里需要操作文件
System.out.println("导出数据"+data+"到文本文件");
return true;
}
}

通过上述代码,我们可以发现,业务代码只需要调用工厂方法,就可以实现不同的导出逻辑,而不需要修改业务代码。这样无论日后增加多少种导出文件类型,都可以通过增加新的导出器来实现,而不需要修改业务代码。业务代码仅根据用户选择的文件格式,调用工厂方法生成对应的导出器,然后调用导出器的导出方法即可。

模式讲解​

工厂方法模式的功能​

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

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

实现成抽象类​

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

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

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

实现成具体的类​

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

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

工厂方法的参数和返回​

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

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

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

这里首先要弄明白一件事情,就是谁在使用工厂方法创建的对象?

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

客户端应该使用Creator对象,或者是使用由Creator创建出来的对象。对于客户端使用Creator对象,这个时候工厂方法创建的对象,是Creator中的某些方法使用;对于使用那些由Creator创建出来的对象,这个时候工厂方法创建的对象,是构成客户端需要的对象的一部分。分别举例来说明。

  1. 客户端使用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();
    //继续业务处理
    }
    }
  2. 客户端使用由Creator创建出来的对象 另外一种是由Creator向客户端返回由“工厂方法创建的对象”来构建的对象,这个时候工厂方法创建的对象,是构成客户端需要的对象的一部分。简单描述这种情况下的代码结构如下:

/**
* 客户端使用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创建的对象,一般客户端不直接使用工厂方法。当然也可以直接把工厂方法暴露给客户端操作,但是一般不这么做。

执行时序图​

由于客户端使用Creator对象有两种典型的情况,因此调用的顺序示意图也分为两种情况。

先看看客户端使用由Creator创建出来的对象情况的调用顺序示意图

design-fm-2.svg

接下来看看客户端使用Creator对象时候的调用顺序示意图

design-fm-3.svg