6.3 模 式 讲 解
6.3.1 认识工厂方法模式
1.工厂方法模式的功能
工厂方法模式的主要功能是让父类在不知道具体实现的情况下,完成自身的功能调用;而具体的实现延迟到子类来实现。
这样在设计的时候,不用去考虑具体的实现,需要某个对象,把它通过工厂方法返回就好了,在使用这些对象实现功能的时候还是通过接口来操作,这类似于IoC/DI的思想,这个在后面将给大家稍详细点介绍。
2.实现成抽象类
工厂方法的实现中,通常父类会是一个抽象类,里面包含创建所需对象的抽象方法,这些抽象方法就是工厂方法。
注意
这里要注意一个问题,子类在实现这些抽象方法的时候,通常并不是真正地由子类来实现具体的功能,而是在子类的方法里面做选择,选择具体的产品实现对象。
父类里面,通常会有使用这些产品对象来实现一定的功能的方法,而且这些方法所实现的功能通常都是公共的功能,不管子类选择了何种具体的产品实现,这些方法的功能总是能正确执行。
3.实现成具体的类
也可以把父类实现成为一个具体的类。这种情况下,通常是在父类中提供获取所需对象的默认实现方法,这样即使没有具体的子类,也能够运行。
通常这种情况还是需要具体的子类来决定具体要如何创建父类所需要的对象。也把这种情况称为工厂方法为子类提供了挂钩。通过工厂方法,可以让子类对象来覆盖父类的实现,从而提供更好的灵活性。
4.工厂方法的参数和返回
工厂方法的实现中,可能需要参数,以便决定到底选用哪一种具体的实现。也就是说通过在抽象方法里面传递参数,在子类实现的时候根据参数进行选择,看看究竟应该创建哪一个具体的实现对象。
一般工厂方法返回的是被创建对象的接口对象,当然也可以是抽象类或者一个具体的类的实例。
5.谁来使用工厂方法创建的对象
这里首先要弄明白一件事情,就是谁在使用工厂方法创建的对象?
事实上,在工厂方法模式里面,应该是Creator中的其他方法在使用工厂方法创建的对象,虽然也可以把工厂方法创建的对象直接提供给Creator外部使用,但工厂方法模式的本意,是由Creator对象内部的方法来使用工厂方法创建的对象,也就是说,工厂方法一般不提供给Creator外部使用。
客户端应该使用Creator对象,或者是使用由Creator创建出来的对象。对于客户端使用Creator对象,这个时候工厂方法创建的对象,是Creator中的某些方法使用;对于使用那些由Creator创建出来的对象,这个时候工厂方法创建的对象,是构成客户端需要的对象的一部分。分别举例来说明。
1)客户端使用Creator对象的情况
比如前面的示例,对于“实现导出数据的业务功能对象”的类ExportOperate,它有一个export的方法,在这个方法里面,需要使用具体的“导出的文件对象的接口对象”ExportFileApi,而ExportOperate是不知道具体的ExportFileApi实现的,那是怎么做的呢?就是定义了一个工厂方法,用来返回ExportFileApi的对象,然后export方法会使用这个工厂方法来获取它所需要的对象,然后执行功能。
这个时候的客户端是怎么做的呢?这个时候客户端主要就是使用ExportOperate的实例来完成它想要完成的功能,也就是客户端使用Creator对象的情况。简单描述这种情况下的代码结构如下:
2)客户端使用由Creator创建出来的对象
另外一种是由Creator向客户端返回由“工厂方法创建的对象”来构建的对象,这个时候工厂方法创建的对象,是构成客户端需要的对象的一部分。简单描述这种情况下的代码结构如下:
提示
小结一下:在工厂方法模式里面,客户端要么使用Creator对象,要么使用Creator创建的对象,一般客户端不直接使用工厂方法。当然也可以直接把工厂方法暴露给客户端操作,但是一般不这么做。
6.工厂方法模式的调用顺序示意图
由于客户端使用Creator对象有两种典型的情况,因此调用的顺序示意图也分为两种情况。
先看看客户端使用由Creator创建出来的对象情况的调用顺序示意图,如图6.5所示。
图6.5 客户端使用由Creator创建出来的对象的调用顺序示意图
接下来看看客户端使用Creator对象时候的调用顺序示意图,如图6.6所示。
6.3.2 工厂方法模式与IoC/DI
IoC——InversionofControl,控制反转。
DI——DependencyInjection,依赖注入。
1.如何理解IoC/DI
要想理解上面两个概念,就必须搞清楚如下的问题:
■ 参与者都有谁?
■ 依赖:谁依赖于谁?为什么需要依赖?
■ 注入:谁注入于谁?到底注入什么?
■ 控制反转:谁控制谁?控制什么?为何叫反转(有反转就应该有正转了)?
■ 依赖注入和控制反转是同一概念吗?
下面就来简要地回答一下上述问题,把这些问题搞明白了,也就明白IoC/DI了。
(1)参与者都有谁:一般有三方参与者,一个是某个对象;另一个是IoC/DI的容器:还有一个是某个对象的外部资源。
解释
解释一下名词,某个对象指的就是任意的、普通的Java对象;IoC/DI的容器简单点说就是指用来实现IoC/DI功能的一个框架程序;对象的外部资源指的就是对象需要的,但是是从对象外部获取的,都统称为资源,比如,对象需要的其他对象,或者是对象需要的文件资源等。
(2)谁依赖于谁:当然是某个对象依赖于IoC/DI的容器。
(3)为什么需要依赖:对象需要IoC/DI的容器来提供对象需要的外部资源。
(4)谁注入于谁:很明显是IoC/DI的容器注入某个对象。
(5)到底注入什么:就是注入某个对象所需要的外部资源。
(6)谁控制谁:当然是IoC/DI的容器来控制对象了。
(7)控制什么:主要是控制对象实例的创建。
(8)为何叫反转:反转是相对于正向而言的,那么什么算是正向的呢?考虑一下常规情况下的应用程序,如果要在A里面使用C,你会怎么做呢?当然是直接去创建C的对象,也就是说,在A类中主动去获取所需要的外部资源C,这种情况被称为正向的。那么什么是反向呢?就是A类不再主动去获取C,而是被动等待,等待IoC/DI的容器获取一个C的实例,然后反向地注入到A类中。
用图例来说明一下。
先看没有IoC/DI的时候,常规的A类使用C的示意图,如图6.7所示。
图6.7 常规的A类使用C的示意图
当有了IoC/DI的容器后,A类不再主动去创建C了,如图6.8所示。
图6.8 A类不再主动创建C
而是被动等待,等待IoC/DI的容器获取一个C的实例,然后反向地注入到A类中,如图6.9所示。
图6.9 有了IoC/DI容器后的程序结构示意图
(9)依赖注入和控制反转是同一概念吗?
根据上面的讲述,应该能看出来,依赖注入和控制反转是对同一件事情的不同描述。从某个方面讲,就是它们描述的角度不同。依赖注入是从应用程序的角度去描述,可以把依赖注入描述得完整点:应用程序依赖容器创建并注入它所需要的外部资源;而控制反转是从容器的角度去描述,描述得完整点就是:容器控制应用程序,由容器反向地向应用程序注入其所需要的外部资源。
延伸
小结:其实IoC/DI对编程带来的最大改变不是在代码上,而是在思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动地等待IoC/DI容器来创建并注入它所需要的资源了。
这么小小的一个改变其实是编程思想的一个大进步,这样就有效地分离了对象和它所需要的外部资源,使得它们松散耦合,有利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。
2.工厂方法模式和IoC/DI的关系
从某个角度讲,工厂方法模式和IoC/DI的思想很类似。
上面讲了,有了IoC/DI后,应用程序就不再主动了,而是被动地等待由容器来注入资源。那么在编写代码的时候,一旦要用到外部资源,就会开一个窗口,让容器能注入进来,也就是提供给容器使用的注入的途径,当然这不是我们的重点,就不去细细讲解了,用setter注入来示例一下,使用IoC/DI的示例代码如下:
接口C的示例代码如下:
从上面的示例代码可以看出,现在在A里面写代码的时候,凡是碰到了需要外部资源,那么就提供注入的途径,要求从外部注入,自己只管使用这些对象。
再来看看工厂方法模式,如何实现上面同样的功能。为了区分,分别取名为A1和C1。这个时候在A1里面要使用C1对象,也不是由A1主动去获取C1对象,而是创建一个工厂方法,类似于一个注入的途径;然后由子类,假设叫A2吧,由A2来获取C1对象,在调用的时候,替换掉A1的相应方法,相当于反向注入回到A1里面。示例代码如下:
子类的示例代码如下:
C1接口和前面的C接口是一样的,C2这个实现类也是空的,只是演示一下,因此就不去展示它们的代码了。
提示
仔细体会上面的示例,对比它们的实现,尤其是从思想层面上,会发现工厂方法模式和IoC/DI的思想是相似的,都是“主动变被动”,进行了“主从换位”,从而获得了更灵活的程序结构。
6.3.3 平行的类层次结构
1.平行的类层次结构的含义
简单点说,假如有两个类层次结构,其中一个类层次中的每个类在另一个类层次中都有一个对应的类的结构,就被称为平行的类层次结构。
举个例子来说,硬盘对象有很多种,如分成台式机硬盘和笔记本硬盘,在台式机硬盘的具体实现上面,又有希捷、西数等不同品牌的实现,同样在笔记本硬盘上,也有希捷、日立、IBM等不同品牌的实现;硬盘对象具有自己的行为,如硬盘能存储数据,也能从硬盘上获取数据,不同的硬盘对象对应的行为对象是不一样的,因为不同的硬盘对象,它的行为的实现方式是不一样的。如果把硬盘对象和硬盘对象的行为分开描述,那么就构成了如图6.10所示的结构。
图6.10 平行的类层次结构示意图
硬盘对象是一个类层次,硬盘的行为也是一个类层次,而且两个类层次中的类是对应的。台式机希捷硬盘对象就对应着硬盘行为里面的台式机希捷硬盘的行为;笔记本IBM硬盘就对应着笔记本IBM硬盘的行为,这就是一种典型的平行的类层次结构。
这种平行的类层次结构用来干什么呢?主要用来把一个类层次中的某些行为分离出来,让类层次中的类把原本属于自己的职责,委托给分离出来的类去实现,从而使得类层次本身变得更简单,更容易扩展和复用。
一般来讲,分离出去的这些类的行为,会对应着类层次结构来组织,从而形成一个新的类层次结构,相当于原来对象行为的类层次结构,而这个层次结构和原来的类层次结构是存在对应关系的,因此被称为平行的类层次结构。
2.工厂方法模式和平行的类层次结构的关系
可以使用工厂方法模式来连接平行的类层次。
如图6.10所示,在每个硬盘对象里面,都有一个工厂方法createHDOperate,通过这个工厂方法,客户端就可以获取一个和硬盘对象相对应的行为对象。在硬盘对象的子类里面,会覆盖父类的工厂方法createHDOperate,以提供与自身相对应的行为对象,从而自然地把两个平行的类层次连接起来使用。
6.3.4 参数化工厂方法
所谓参数化工厂方法指的就是:通过给工厂方法传递参数,让工厂方法根据参数的不同来创建不同的产品对象,这种情况就被称为参数化工厂方法。当然工厂方法创建的不同的产品必须是同一个Product类型的。
来改造前面的示例,现在由一个工厂方法来创建ExportFileApi这个产品的对象,但是ExportFileApi接口的具体实现很多,为了方便创建的选择,直接从客户端传入一个参数,这样在需要创建ExportFileApi对象的时候,就把这个参数传递给工厂方法,让工厂方法来实例化具体的ExportFileApi实现对象。
还是看看代码示例会比较清楚。
(1)先来看Product的接口,就是ExportFileApi接口,和前面的示例相比没有任何变化,只是为了方便大家查看,这里重复一下。示例代码如下:
(2)同样提供保存成文本文件和保存成数据库备份文件的实现,和前面的示例相比没有任何变化。示例代码如下:
(3)接下来该看看ExportOperate类了,这个类的变化大致如下。
■ ExportOperate类中的创建产品的工厂方法,通常需要提供默认的实现,不再抽象了,也就是变成了正常方法。
■ ExportOperate类也不再定义成抽象类了,因为有了默认的实现,客户端可能需要直接使用这个对象。
■ 设置一个导出类型的参数,通过export方法从客户端传入。
看看代码吧,示例代码如下:
(4)此时的客户端非常简单,直接使用ExportOperate类。示例代码如下:
测试看看,然后修改一下客户端的参数,体会一下通过参数来选择具体的导出实现的过程。
提示
这是一种很常见的参数化工厂方法的实现方式,但是也还是有把参数化工厂方法实现成为抽象的,这点要注意,并不是说参数化工厂方法就不能实现成为抽象类了。只是一般情况下,参数化工厂方法,在父类都会提供默认的实现。
(5)扩展新的实现。
使用参数化工厂方法,扩展起来会非常容易,已有的代码都不会改变,只要新加入一个子类来提供新的工厂方法实现,然后在客户端使用这个新的子类即可。
这种实现方式还有一个有意思的功能,就是子类可以选择性覆盖,不想覆盖的功能还可以返回去让父类来实现,很有意思。
扩展一个导出成xml文件的示例代码如下:
然后扩展ExportOperate类,来加入新的实现。示例代码如下:
看看此时的客户端,也非常简单,只是在变换传入的参数。示例代码如下:
对应的测试结果如下:
通过上面的示例,好好体会一下参数化工厂方法的实现和带来的好处。
6.3.5 工厂方法模式的优缺点
工厂方法模式的优点
■ 可以在不知具体实现的情况下编程
工厂方法模式可以让你在实现功能的时候,如果需要某个产品对象,只需要使用产品的接口即可,而无需关心具体的实现。选择具体实现的任务延迟到子类去完成。
■ 更容易扩展对象的新版本
工厂方法给子类提供了一个挂钩,使得扩展新的对象版本变得非常容易。比如上面示例的参数化工厂方法实现中,扩展一个新的导出xml文件格式的实现,已有的代码都不会改变,只要新加入一个子类来提供新的工厂方法实现,然后在客户端使用这个新的子类即可。
提示
另外这里提到的挂钩,就是我们经常说的钩子方法(hook),这个会在后面讲模板方法模式的时候详细点说明。
■ 连接平行的类层次
工厂方法除了创造产品对象外,在连接平行的类层次上也大显身手。这个在前面已经详细讲述了。
工厂方法模式的缺点
■ 具体产品对象和工厂方法的耦合性。
在工厂方法模式中,工厂方法是需要创建产品对象的,也就是需要选择具体的产品对象,并创建它们的实例,因此具体产品对象和工厂方法是耦合的。
6.3.6 思考工厂方法模式
1.工厂方法模式的本质
工厂方法模式的本质:延迟到子类来选择实现。
仔细体会前面的示例,你会发现,工厂方法模式中的工厂方法,在真正实现的时候,一般是先选择具体使用哪一个具体的产品实现对象,然后创建这个具体产品对象的示例,最后就可以返回去了。也就是说,工厂方法本身并不会去实现产品接口,具体的产品实现是已经写好了的,工厂方法只要去选择实现就好了。
有些朋友可能会说,这不是跟简单工厂一样吗?
从本质上讲,它们确实是非常类似的,在具体实现上都是“选择实现”。但是也存在不同点,简单工厂是直接在工厂类里面进行“选择实现”;而工厂方法会把这个工作延迟到子类来实现,工厂类里面使用工厂方法的地方是依赖于抽象而不是具体的实现,从而使得系统更加灵活,具有更好的可维护性和可扩展性。
其实如果把工厂模式中的Creator退化一下,只提供工厂方法,而且这些工厂方法还都提供默认的实现,那不就变成简单工厂了吗?比如把刚才示范参数化工厂方法的例子代码拿过来再简化一下,你就能看出来,写得跟简单工厂是差不多的。示例代码如下:
看完上述代码,会体会到简单工厂和工厂方法模式是有很大相似性的了吧,从某个角度来讲,可以认为简单工厂就是工厂方法模式的一种特例,因此它们的本质是类似的,也就不足为奇了。
2.对设计原则的体现
工厂方法模式很好地体现了“依赖倒置原则”。
依赖倒置原则告诉我们“要依赖抽象,不要依赖于具体类”,简单点说就是:不能让高层组件依赖于低层组件,而且不管高层组件还是低层组件,都应该依赖于抽象。
比如前面的示例,实现客户端请求操作的ExportOperate就是高层组件;而具体实现数据导出的对象就是低层组件,比如ExportTxtFile、ExportDB;而ExportFileApi接口就相当于是那个抽象。
对于ExportOperate来说,它不关心具体的实现方式,它只是“面向接口编程”;对于具体的实现来说,它只关心自己“如何实现接口”所要求的功能。
那么倒置的是什么呢?倒置的是这个接口的“所有权”。事实上,ExportFileApi接口中定义的功能,都是由高层组件ExportOperate来提出的要求,也就是说接口中的功能,是高层组件需要的功能。但是高层组件只是提出要求,并不关心如何实现,而底层组件,就是来真正实现高层组件所要求的接口功能的。因此看起来,低层实现的接口的所有权并不在底层组件手中,而是倒置到高层组件去了。
3.何时选用工厂方法模式
建议在以下情况中选用工厂方法模式。
■ 如果一个类需要创建某个接口的对象,但是又不知道具体的实现,这种情况可以选用工厂方法模式,把创建对象的工作延迟到子类中去实现。
■ 如果一个类本身就希望由它的子类来创建所需的对象的时候,应该使用工厂方法模式。
6.3.7 相关模式
■ 工厂方法模式和抽象工厂模式
这两个模式可以组合使用,具体的放到抽象工厂模式中去讲。
■ 工厂方法模式和模板方法模式
这两个模式外观类似,都有一个抽象类,然后由子类来提供一些实现,但是工厂方法模式的子类专注的是创建产品对象,而模板方法模式的子类专注的是为固定的算法骨架提供某些步骤的实现。
这两个模式可以组合使用,通常在模板方法模式里面,使用工厂方法来创建模板方法需要的对象。