摘要
JDK:1.8.0_202
# 一:问题
实现导出功能,调用方选择怎么样的导出格式,虽然最后导出的都是一个文件,而系统一开始并不知道要导出成为怎么样的文件,因此应该有一个统一的接口,来描述系统最后生成的对象,并操作输出的文件。
先把导出的文件对象的接口定义出来,示例代码如下:
/**
* 导出的文件对象的接口
*/
public interface ExportFileApi {
/**
* 导出内容成为文件
*
* @param data 示意:需要保存的数据
* @return 是否导出成功
*/
boolean export(String data);
}
2
3
4
5
6
7
8
9
10
11
12
13
对于实现导出数据的业务功能对象,它应该根据需要来创建相应的ExportFileApi的实现对象,因为特定的ExportFileApi的实现是与具体的业务相关的。但是对于实现导出数据的业务功能对象而言,它并不知道应该创建哪一个ExportFileApi的实现对象,也不知道如何创建。
也就是说:对于实现导出数据的业务功能对象,它需要创建ExportFileApi的具体实例对象,但是它只知道ExportFileApi接口,而不知道其具体的实现。那该怎么办呢?
# 二:解决方案
# 2.1 工厂方法模式
用来解决上述问题的一个合理的解决方案就是工厂方法模式
工厂方法模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类,Factory Method使一个类的实例化延迟到其子类。
仔细分析上面的问题,事实上在实现导出数据的业务功能对象里面,根本就不知道究竟要使用哪一种导出文件的格式,因此这个对象本就不应该和具体的导出文件的对象耦合在一起,它只需要面向导出的文件对象的接口就好了。
但是这样一来,又有新的问题产生了:接口是不能直接使用的,需要使用具体的接口实现对象的实例
这不是自相矛盾吗?要求面向接口,不让和具体的实现耦合,但是又需要创建接口的具体实现对象的实例。怎么解决这个矛盾呢?
工厂方法模式的解决思路很有意思,那就是不解决,采取无为而治的方式:不是需要接口对象吗,那就定义一个方法来创建;可是事实上它自己是不知道如何创建这个接口对象的,没有关系,那就定义成抽象方法就好了,自己实现不了,那就让子类来实现,这样这个对象本身就可以只是面向接口编程,而无需关心到底如何创建接口对象了
# 2.2 模式结构和说明
工厂方法模板结构如图所示:
- Product:定义工厂方法所创建的对象的接口,也就是实际需要使用的对象的接口
- ConcreteProduct:具体的Product接口的实现对象
- Creator:创建器,声明工厂方法,工厂方法通常会返回一个Product类型的实例对象,而且多是抽象方法。也可以在Creator里面提供工厂方法的默认实现,让工厂方法返回一个缺省的Product类型的实例对象
- ConcreteCreator:具体的创建器对象,覆盖实现Creator定义的工厂方法,返回具体的Product实例
# 2.3 示例代码
接口:(Product.java)
/**
* 工厂方法所创建的对象的接口
*/
public interface Product {
// 可以定义 Product 的属性和方法
}
2
3
4
5
6
7
8
实现类:(ConcreteProduct.java)
/**
* 具体的Product对象
*/
public class ConcreteProduct implements Product {
// 实现Product要求的方法
}
2
3
4
5
6
7
8
创建器:(Creator.java)
/**
* 创建器,声明工厂方法
*/
public abstract class Creator {
/**
* 创建Product的工厂方法
*
* @return Product对象
*/
protected abstract Product factoryMethod();
/**
* 示意方法,实现某些功能的方法
*/
public void someOperation() {
//通常在这些方法实现中,需要调用工厂方法来获取Product对象
Product product = factoryMethod();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
创建器实现:(ConcreteCreator.java)
/**
* 具体的创建器实现对象
*/
public class ConcreteCreator extends Creator {
@Override
protected Product factoryMethod() {
//重定义工厂方法,返回一个具体的Product对象
return new ConcreteProduct();
}
}
2
3
4
5
6
7
8
9
10
# 2.4 使用工厂方法模式来实现示例
要使用工厂方法模式来实现示例,先来按照工厂方法模式的结构,对应出哪些是被创建的Product,哪些是Creator。分析要求实现的功能,导出的文件对象接口ExportFileApi就相当于是Product,而用来实现导出数据的业务功能对象就相当于Creator。把Product和Creator分开过后,就可以分别来实现它们了
接口:(ExportFileApi.java)
/**
* 导出的文件对象的接口
*/
public interface ExportFileApi {
/**
* 导出内容成为文件
*
* @param data 示意:需要保存的数据
* @return 是否导出成功
*/
boolean export(String data);
}
2
3
4
5
6
7
8
9
10
11
12
13
实现类:(ExportTxtFile.java 和 ExportDB.java)
/**
* 导出成文本文件格式的对象
*/
public class ExportTxtFile implements ExportFileApi {
@Override
public boolean export(String data) {
System.out.println("导出数据" + data + "到文本文件");
return true;
}
}
2
3
4
5
6
7
8
9
10
/**
* 导出成数据库备份文件形式的对象
*/
public class ExportDB implements ExportFileApi {
@Override
public boolean export(String data) {
System.out.println("导出数据" + data + "到数据库备份文件");
return true;
}
}
2
3
4
5
6
7
8
9
10
创建器:(ExportOperate .java)
/**
* 实现导出数据的业务功能对象
*/
public abstract class ExportOperate {
/**
* 导出文件
*
* @param data 需要保存的数据
* @return 是否成功导出文件
*/
public boolean export(String data) {
//使用工厂方法
ExportFileApi api = factoryMethod();
return api.export(data);
}
/**
* 工厂方法,创建导出的文件对象的接口对象
*
* @return 导出的文件对象的接口对象
*/
protected abstract ExportFileApi factoryMethod();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
创建器实现:(ExportTxtFileOperate.java 和 ExportDBOperate.java)
/**
* 具体的创建器实现对象,实现创建导出成文本文件格式的对象
*/
public class ExportTxtFileOperate extends ExportOperate {
protected ExportFileApi factoryMethod() {
//创建导出成文本文件格式的对象
return new ExportTxtFile();
}
}
2
3
4
5
6
7
8
/**
* 具体的创建器实现对象,实现创建导出成数据库备份文件形式的对象
*/
public class ExportDBOperate extends ExportOperate {
protected ExportFileApi factoryMethod() {
//创建导出成数据库备份文件形式的对象
return new ExportDB();
}
}
2
3
4
5
6
7
8
9
客户端:(Client.java)
public class Client {
public static void main(String[] args) {
//创建需要使用的Creator对象
ExportOperate operate = new ExportDBOperate();
//调用输出数据的功能方法
operate.export("测试数据"); // 导出数据测试数据到数据库备份文件
}
}
2
3
4
5
6
7
8
# 三:模式讲解
# 3.1 认识工厂方法
1. 模式的功能
工厂方法的主要功能是让父类在不知道具体实现的情况下,完成自身的功能调用,而具体的实现延迟到子类来实现。
这样在设计的时候,不用去考虑具体的实现,需要某个对象,把它通过工厂方法返回就好了,在使用这些对象实现功能的时候还是通过接口来操作,这非常类似于IoC/DI的思想,这个在后面给大家稍详细点介绍一下。
2. 实现成抽象类
工厂方法的实现中,通常父类会是一个抽象类,里面包含创建所需对象的抽象方法,这些抽象方法就是工厂方法。
这里要注意一个问题,子类在实现这些抽象方法的时候,通常并不是真的由子类来实现具体的功能,而是在子类的方法里面做选择,选择具体的产品实现对象。
父类里面,通常会有使用这些产品对象来实现一定的功能的方法,而且这些方法所实现的功能通常都是公共的功能,不管子类选择了何种具体的产品实现,这些方法的功能总是能正确执行。
3. 实现成具体的类
当然也可以把父类实现成为一个具体的类,这种情况下,通常是在父类中提供获取所需对象的默认实现方法,这样就算没有具体的子类,也能够运行。
通常这种情况还是需要具体的子类来决定具体要如何创建父类所需要的对象。也把这种情况称为工厂方法为子类提供了挂钩,通过工厂方法,可以让子类对象来覆盖父类的实现,从而提供更好的灵活性。
4. 工厂方法的参数和返回
工厂方法的实现中,可能需要参数,以便决定到底选用哪一种具体的实现。也就是说通过在抽象方法里面传递参数,在子类实现的时候根据参数进行选择,看看究竟应该创建哪一个具体的实现对象。
一般工厂方法返回的是被创建对象的接口对象,当然也可以是抽象类或者一个具体的类的实例。
工厂方法模式很好的体现了 "依赖倒置原则"。
依赖倒置原则告诉我们 "要依赖抽象,不要依赖于具体类",简单点说就是:不能让高层组件依赖于低层组件,而且不管高层组件还是低层组件,都应该依赖于抽象。
# 3.2 优缺点
1. 可以在不知具体实现的情况下编程
工厂方法模式可以让你在实现功能的时候,如果需要某个产品对象,只需要使用产品的接口即可,而无需关心具体的实现。选择具体实现的任务延迟到子类去完成。
更容易扩展对象的新版本。
工厂方法给子类提供了一个挂钩,使得扩展新的对象版本变得非常容易。比如上面示例的参数化工厂方法实现中,扩展一个新的导出Xml文件格式的实现,已有的代码都不会改变,只要新加入一个子类来提供新的工厂方法实现,然后在客户端使用这个新的子类即可。
另外这里提到的挂钩,就是我们经常说的钩子方法(hook),这个会在后面讲模板方法模式的时候详细点说明。
2. 具体产品对象和工厂方法的耦合性
在工厂方法模式里面,工厂方法是需要创建产品对象的,也就是需要选择具体的产品对象,并创建它们的实例,因此具体产品对象和工厂方法是耦合的。
# 3.3 对设计原则的体现
工厂方法模式很好的体现了 "依赖倒置原则"。
依赖倒置原则告诉我们“要依赖抽象,不要依赖于具体类,简单点说就是:不能让高层组件依赖于低层组件,而且不管高层组件还是低层组件,都应该依赖于抽象。
比如前面的示例,实现客户端请求操作的ExportOperate就是高层组件;而具体实现数据导出的对象就是低层组件,比如ExportTxtFile、ExportDB;而ExportFileApi接口就相当于是那个抽象。
对于ExportOperate来说,它不关心具体的实现方式,它只是 "面向接口编程";对于具体的实现来说,它只关心自己 "如何实现接口" 所要求的功能。
那么倒置的是什么呢?倒置的是这个接口的 "所有权"。事实上,ExportFileApi接口中定义的功能,都是由高层组件ExportOperate来提出的要求,也就是说接口中的功能,是高层组件需要的功能。但是高层组件只是提出要求,并不关心如何实现,而低层组件,就是来真正实现高层组件所要求的接口功能的。因此看起来,低层实现的接口的所有权并不在底层组件手中,而是倒置到高层组件去了。
# 3.4 何时选用工厂方法模式
建议在如下情况中,选用工厂方法模式:
如果一个类需要创建某个接口的对象,但是又不知道具体的实现,这种情况可以选用工厂方法模式,把创建对象的工作延迟到子类去实现。
如果一个类本身就希望,由它的子类来创建所需的对象的时候,应该使用工厂方法模式。
# 3.5 相关模式
简单工厂和工厂方法模式也是非常类似的。工厂方法的本质也是用来选择实现的,跟简单工厂的区别在于工厂方法是把选择具体实现的功能延迟到子类去实现。如果把工厂方法中选择的实现放到父类直接实现,那就等同于简单工厂
# 四:JDK
- java.util.Calendar#getInstance() (opens new window)
- java.util.ResourceBundle#getBundle() (opens new window)
- java.text.NumberFormat#getInstance() (opens new window)
- java.nio.charset.Charset#forName() (opens new window)
- java.net.URLStreamHandlerFactory (opens new window)
- java.util.EnumSet#of() (opens new window)
- javax.xml.bind.JAXBContext#createMarshaller() (opens new window)