研磨设计模式之 工厂方法模式-5
3.3 平行的类层次结构
(1)什么是平行的类层次结构呢?
简单点说,假如有两个类层次结构,其中一个类层次中的每个类在另一个类层次中都有一个对应的类的结构,就被称为平行的类层次结构。
举个例子来说,硬盘对象有很多种,如分成台式机硬盘和笔记本硬盘,在台式机硬盘的具体实现上面,又有希捷、西数等不同品牌的实现,同样在笔记本硬盘上,也有希捷、日立、IBM等不同品牌的实现;硬盘对象具有自己的行为,如硬盘能存储数据,也能从硬盘上获取数据,不同的硬盘对象对应的行为对象是不一样的,因为不同的硬盘对象,它的行为的实现方式是不一样的。如果把硬盘对象和硬盘对象的行为分开描述,那么就构成了如图10所示的结构:
图10 平行的类层次结构示意图
硬盘对象是一个类层次,硬盘的行为这边也是一个类层次,而且两个类层次中的类是对应的。台式机西捷硬盘对象就对应着硬盘行为里面的台式机西捷硬盘的行为;笔记本IBM硬盘就对应着笔记本IBM硬盘的行为,这就是一种典型的平行的类层次结构。
这种平行的类层次结构用来干什么呢?主要用来把一个类层次中的某些行为分离出来,让类层次中的类把原本属于自己的职责,委托给分离出来的类去实现,从而使得类层次本身变得更简单,更容易扩展和复用。
一般来讲,分离出去的这些类的行为,会对应着类层次结构来组织,从而形成一个新的类层次结构,相当于原来对象的行为的这么一个类层次结构,而这个层次结构和原来的类层次结构是存在对应关系的,因此被称为平行的类层次结构。
(2)工厂方法模式跟平行的类层次结构有何关系呢?
可以使用工厂方法模式来连接平行的类层次。
看上面的示例图10,在每个硬盘对象里面,都有一个工厂方法createHDOperate,通过这个工厂方法,客户端就可以获取一个跟硬盘对象相对应的行为对象。在硬盘对象的子类里面,会覆盖父类的工厂方法createHDOperate,以提供跟自身相对应的行为对象,从而自然的把两个平行的类层次连接起来使用。
3.4 参数化工厂方法
所谓参数化工厂方法指的就是:通过给工厂方法传递参数,让工厂方法根据参数的不同来创建不同的产品对象,这种情况就被称为参数化工厂方法。当然工厂方法创建的不同的产品必须是同一个Product类型的。
来改造前面的示例,现在有一个工厂方法来创建ExportFileApi这个产品的对象,但是ExportFileApi接口的具体实现很多,为了方便创建的选择,直接从客户端传入一个参数,这样在需要创建ExportFileApi对象的时候,就把这个参数传递给工厂方法,让工厂方法来实例化具体的ExportFileApi实现对象。
还是看看代码示例会比较清楚。
(1)先来看Product的接口,就是ExportFileApi接口,跟前面的示例没有任何变化,为了方便大家查看,这里重复一下,示例代码如下:
/** * 导出的文件对象的接口 */ public interface ExportFileApi { /** * 导出内容成为文件 * @param data 示意:需要保存的数据 * @return 是否导出成功 */ public boolean export(String data); } |
(2)同样提供保存成文本文件和保存成数据库备份文件的实现,跟前面的示例没有任何变化,示例代码如下:
public class ExportTxtFile implements ExportFileApi{ public boolean export(String data) { //简单示意一下,这里需要操作文件 System.out.println("导出数据"+data+"到文本文件"); return true; } } public class ExportDB implements ExportFileApi{ public boolean export(String data) { //简单示意一下,这里需要操作数据库和文件 System.out.println("导出数据"+data+"到数据库备份文件"); return true; } } |
(3)接下来该看看ExportOperate类了,这个类的变化大致如下:
- ExportOperate类中的创建产品的工厂方法,通常需要提供默认的实现,不抽象了,也就是变成正常方法
- ExportOperate类也不再定义成抽象类了,因为有了默认的实现,客户端可能需要直接使用这个对象
- 设置一个导出类型的参数,通过export方法从客户端传入
看看代码吧,示例代码如下:
/** * 实现导出数据的业务功能对象 */ public class ExportOperate { /** * 导出文件 * @param type 用户选择的导出类型 * @param data 需要保存的数据 * @return 是否成功导出文件 */ public boolean export(int type,String data){ //使用工厂方法 ExportFileApi api = factoryMethod(type); return api.export(data); } /** * 工厂方法,创建导出的文件对象的接口对象 * @param type 用户选择的导出类型 * @return 导出的文件对象的接口对象 */ protected ExportFileApi factoryMethod(int type){ ExportFileApi api = null; //根据类型来选择究竟要创建哪一种导出文件对象 if(type==1){ api = new ExportTxtFile(); }else if(type==2){ api = new ExportDB(); } return api; } } |
(4)此时的客户端,非常简单,直接使用ExportOperate类,示例代码如下:
public class Client { public static void main(String[] args) { //创建需要使用的Creator对象 ExportOperate operate = new ExportOperate(); //调用输出数据的功能方法,传入选择到处类型的参数 operate.export(1,"测试数据"); } } |
测试看看,然后修改一下客户端的参数,体会一下通过参数来选择具体的导出实现的过程。这是一种很常见的参数化工厂方法的实现方式,但是也还是有把参数化工厂方法实现成为抽象的,这点要注意,并不是说参数化工厂方法就不能实现成为抽象类了。只是一般情况下,参数化工厂方法,在父类都会提供默认的实现。
(5)扩展新的实现
使用参数化工厂方法,扩展起来会非常容易,已有的代码都不会改变,只要新加入一个子类来提供新的工厂方法实现,然后在客户端使用这个新的子类即可。
这种实现方式还有一个有意思的功能,就是子类可以选择性覆盖,不想覆盖的功能还可以返回去让父类来实现,很有意思。
先扩展一个导出成xml文件的实现,试试看,示例代码如下:
/** * 导出成xml文件的对象 */ public class ExportXml implements ExportFileApi{ public boolean export(String data) { //简单示意一下 System.out.println("导出数据"+data+"到XML文件"); return true; } } |
然后扩展ExportOperate类,来加入新的实现,示例代码如下:
/** * 扩展ExportOperate对象,加入可以导出XML文件 */ public class ExportOperate2 extends ExportOperate{ /** * 覆盖父类的工厂方法,创建导出的文件对象的接口对象 * @param type 用户选择的导出类型 * @return 导出的文件对象的接口对象 */ protected ExportFileApi factoryMethod(int type){ ExportFileApi api = null; //可以全部覆盖,也可以选择自己感兴趣的覆盖, //这里只想添加自己新的实现,其它的不管 if(type==3){ api = new ExportXml(); }else{ //其它的还是让父类来实现 api = super.factoryMethod(type); } return api; } } |
看看此时的客户端,也非常简单,只是在变换传入的参数,示例代码如下:
public class Client { public static void main(String[] args) { //创建需要使用的Creator对象 ExportOperate operate = new ExportOperate2(); //下面变换传入的参数来测试参数化工厂方法 operate.export(1,"Test1"); operate.export(2,"Test2"); operate.export(3,"Test3"); } } |
对应的测试结果如下:
导出数据Test1到文本文件 导出数据Test2到数据库备份文件 导出数据Test3到XML文件 |
通过上面的示例,好好体会一下参数化工厂方法的实现和带来的好处。
3.5 工厂方法模式的优缺点
- 可以在不知具体实现的情况下编程
工厂方法模式可以让你在实现功能的时候,如果需要某个产品对象,只需要使用产品的接口即可,而无需关心具体的实现。选择具体实现的任务延迟到子类去完成。 - 更容易扩展对象的新版本
工厂方法给子类提供了一个挂钩,使得扩展新的对象版本变得非常容易。比如上面示例的参数化工厂方法实现中,扩展一个新的导出Xml文件格式的实现,已有的代码都不会改变,只要新加入一个子类来提供新的工厂方法实现,然后在客户端使用这个新的子类即可。
另外这里提到的挂钩,就是我们经常说的钩子方法(hook),这个会在后面讲模板方法模式的时候详细点说明。 - 连接平行的类层次
工厂方法除了创造产品对象外,在连接平行的类层次上也大显身手。这个在前面已经详细讲述了。 - 具体产品对象和工厂方法的耦合性
在工厂方法模式里面,工厂方法是需要创建产品对象的,也就是需要选择具体的产品对象,并创建它们的实例,因此具体产品对象和工厂方法是耦合的。
3.6 思考工厂方法模式
1:工厂方法模式的本质
工厂方法模式的本质:延迟到子类来选择实现。
仔细体会前面的示例,你会发现,工厂方法模式中的工厂方法,在真正实现的时候,一般是先选择具体使用哪一个具体的产品实现对象,然后创建这个具体产品对象的示例,然后就可以返回去了。也就是说,工厂方法本身并不会去实现产品接口,具体的产品实现是已经写好了的,工厂方法只要去选择实现就好了。
有些朋友可能会说,这不是跟简单工厂一样吗?
确实从本质上讲,它们是非常类似的,具体实现上都是在“选择实现”。但是也存在不同点,简单工厂是直接在工厂类里面进行“选择实现”;而工厂方法会把这个工作延迟到子类来实现,工厂类里面使用工厂方法的地方是依赖于抽象而不是具体的实现,从而使得系统更加灵活,具有更好的可维护性和可扩展性。
其实如果把工厂模式中的Creator退化一下,只提供工厂方法,而且这些工厂方法还都提供默认的实现,那不就变成了简单工厂了吗?比如把刚才示范参数化工厂方法的例子代码拿过来再简化一下,你就能看出来,写得跟简单工厂是差不多的,示例代码如下:
看完上述代码,会体会到简单工厂和工厂方法模式是有很大相似性的了吧,从某个角度来讲,可以认为简单工厂就是工厂方法模式的一种特例,因此它们的本质是类似的,也就不足为奇了。
2:对设计原则的体现
工厂方法模式很好的体现了“依赖倒置原则”。
依赖倒置原则告诉我们“要依赖抽象,不要依赖于具体类”,简单点说就是:不能让高层组件依赖于低层组件,而且不管高层组件还是低层组件,都应该依赖于抽象。
比如前面的示例,实现客户端请求操作的ExportOperate就是高层组件;而具体实现数据导出的对象就是低层组件,比如ExportTxtFile、ExportDB;而ExportFileApi接口就相当于是那个抽象。
对于ExportOperate来说,它不关心具体的实现方式,它只是“面向接口编程”;对于具体的实现来说,它只关心自己“如何实现接口”所要求的功能。
那么倒置的是什么呢?倒置的是这个接口的“所有权”。事实上,ExportFileApi接口中定义的功能,都是由高层组件ExportOperate来提出的要求,也就是说接口中的功能,是高层组件需要的功能。但是高层组件只是提出要求,并不关心如何实现,而低层组件,就是来真正实现高层组件所要求的接口功能的。因此看起来,低层实现的接口的所有权并不在底层组件手中,而是倒置到高层组件去了。
3:何时选用工厂方法模式
建议在如下情况中,选用工厂方法模式:
- 如果一个类需要创建某个接口的对象,但是又不知道具体的实现,这种情况可以选用工厂方法模式,把创建对象的工作延迟到子类去实现
- 如果一个类本身就希望,由它的子类来创建所需的对象的时候,应该使用工厂方法模式
3.7 相关模式
- 工厂方法模式和抽象工厂模式
这两个模式可以组合使用,具体的放到抽象工厂模式中去讲。 - 工厂方法模式和模板方法模式
这两个模式外观类似,都是有一个抽象类,然后由子类来提供一些实现,但是工厂方法模式的子类专注的是创建产品对象,而模板方法模式的子类专注的是为固定的算法骨架提供某些步骤的实现。
这两个模式可以组合使用,通常在模板方法模式里面,使用工厂方法来创建模板方法需要的对象。
=============工厂方法模式结束,谢谢观看!=================