2.3 模 式 讲 解
2.3.1 典型疑问
首先来解决一个常见的问题:可能有朋友会认为,上面示例中的简单工厂看起来不就是把客户端里面的“new Impl()”移动到简单工厂里面吗?不还是一样通过new一个实现类来得到接口吗?把“new Impl()”这句话放到客户端和放到简单工厂里面有什么不同吗?
提示
理解这个问题的重点就在于理解简单工厂所处的位置。
根据前面的学习,我们知道接口是用来封装隔离具体的实现的,目标就是不要让客户端知道封装体内部的具体实现。简单工厂的位置是位于封装体内的,也就是简单工厂是跟接口和具体的实现在一起的,算是封装体内部的一个类,所以简单工厂知道具体的实现类是没有关系的。重新整理一下简单工厂的结构图,如图2.7所示。
图2.7 整理后的简单工厂结构
图2.7中的虚线框,就好比是一个组件的包装边界,表示接口、实现类和工厂类组合成了一个组件。在这个封装体里面,只有接口和工厂是对外的,也就是让外部知道并使用的,所以故意漏了一些在虚线框外,而具体的实现类是不对外的,被完全包含在虚线框内。
对于客户端而言,只是知道了接口Api和简单工厂Factory,通过Factory就可以获得Api了,这样就达到了让Client在不知道具体实现类的情况下获取接口Api。
所以看似简单地将new Impl()这句话从客户端里面移动到了简单工厂里面,其实是有了质的变化的。
2.3.2 认识简单工厂
1.简单工厂的功能
工厂嘛,就是用来创造东西的。在Java里面,通常情况下是用来创造接口的,但是也可以创造抽象类,甚至是一个具体的类实例。
注意
一定要注意,虽然前面的示例是利用简单工厂来创建的接口,但是也可以用简单工厂来创建抽象类或普通类的实例。
2.静态工厂
使用简单工厂的时候,通常不用创建简单工厂类的类实例,没有创建实例的必要。因此可以把简单工厂类实现成一个工具类,直接使用静态方法就可以了。也就是说简单工厂的方法通常是静态的,所以也被称为静态工厂。如果要防止客户端无谓地创造简单工厂实例,还可以把简单工厂的构造方法私有化了。
3.万能工厂
一个简单工厂可以包含很多用来构造东西的方法,这些方法可以创建不同的接口、抽象类或者是类实例。一个简单工厂理论上可以构造任何东西,所以又称之为“万能工厂”。
虽然上面的实例在简单工厂里面只有一个方法,但事实上,是可以有很多这样的创建方法的,这点要注意。
4.简单工厂创建对象的范围
虽然从理论上讲,简单工厂什么都能创建,但对于简单工厂可创建对象的范围,通常不要太大,建议控制在一个独立的组件级别或者一个模块级别,也就是一个组件或模块简单工厂。否则这个简单工厂类会职责不明,有点大杂烩的感觉。
5.简单工厂的调用顺序示意图
简单工厂的调用顺序如图2.8所示:
图2.8 简单工厂的调用顺序示意图
6.简单工厂命名的建议
■ 类名称建议为“模块名称+Factory”。比如,用户模块的工厂就称为UserFactory。
■ 方法名称通常为“get+接口名称”或者是“create+接口名称”。比如,有一个接口名称为UserEbi,那么方法名称通常为getUserEbi或者是createUserEbi。
当然,也有一些朋友习惯于把方法名称命名为“new+接口名称”。比如,newUserEbi,我们不提倡这样做。因为new在Java中代表特定的含义,而且通过简单工厂的方法来获取对象实例,并不一定每次都是要new一个新的实例。如果使用newUserEbi,会给人错觉,好像每次都是new一个新的实例一样。
2.3.3 简单工厂中方法的写法
虽然说简单工厂的方法大多是用来创建接口的,但是仔细分析就会发现,真正能实现功能的是具体的实现类,这些实现类是已经做好的,并不是真的要靠简单工厂来创造出来的,简单工厂的方法无外乎就是:实现了选择一个合适的实现类来使用。
所以说简单工厂方法的内部主要实现的功能是“选择合适的实现类”来创建实例对象。既然要实现选择,那么就需要选择的条件或者是选择的参数,选择条件或者是参数的来源通常又有以下几种。
■ 来源于客户端,由Client来传入参数
■ 来源于配置文件,从配置文件获取用于判断的值
■ 来源于程序运行期的某个值,比如从缓存中获取某个运行期的值
下面来看示例,看看由客户端来传入参数,如何写简单工厂中的方法。
(1)在2.3.3节的示例上再添加一个实现,称为Impl2,示例代码如下:
(2)现在对Api这个接口,有了两种实现,那么工厂类该怎么办呢?到底如何选择呢?不可能两个同时使用吧,看看新的工厂类,示例代码如下:
(3)客户端没有什么变化,只是在调用Factory的createApi方法的时候需要传入参数,示例代码如下:
(4)要注意这种方法有一个缺点。
由于是从客户端在调用工厂的时候传入选择的参数,这就说明客户端必须知道每个参数的含义,也需要理解每个参数对应的功能处理。这就要求必须在一定程度上,向客户暴露一定的内部实现细节。
2.3.4 可配置的简单工厂
现在已经学会通过简单工厂来选择具体的实现类了,可是还有问题。比如,在现在的实现中,再新增加一种实现,该怎么办呢?
那就需要修改工厂类,才能把新的实现添加到现有的系统中。比如现在新增加了一个实现类Impl3,那么就需要类似下面这样来修改工厂类:
每次新增加一个实现类都来修改工厂类的实现,肯定不是一个好的实现方式。那么现在希望新增加了实现类过后不修改工厂类,该怎么办呢?
一个解决的方法就是使用配置文件,当有了新的实现类后,只要在配置文件里面配置上新的实现类即可。在简单工厂的方法里面可以使用反射,当然也可以使用IoC/DI(控制反转/依赖注入,这个不在这里讨论)来实现。
下面来看看如何使用反射加上配置文件,来实现添加新的实现类后,无须修改代码,就能把这个新的实现类加入应用中。
(1)配置文件用最简单的properties文件,实际开发中多是xml配置。定义一个名称为“FactoryTest.properties”的配置文件,放置到Factory同一个包下面,内容如下:
如果新添加了实现类,修改这里的配置即可,就不需要修改程序了。
(2)此时的工厂类实现如下:
(3)此时的客户端就变得很简单了,不再需要传入参数,代码示例如下:
2.3.5 简单工厂的优缺点
简单工厂有以下优点。
■ 帮助封装
简单工厂虽然很简单,但是非常友好地帮助我们实现了组件的封装,然后让组件外部能真正面向接口编程。
■ 解耦
通过简单工厂,实现了客户端和具体实现类的解耦。
如同上面的例子,客户端根本就不知道具体是由谁来实现,也不知道具体是如何实现的,客户端只是通过工厂获取它需要的接口对象。
简单工厂有以下缺点。
■ 可能增加客户端的复杂度
如果通过客户端的参数来选择具体的实现类,那么就必须让客户端能理解各个参数所代表的具体功能和含义,这样会增加客户端使用的难度,也部分暴露了内部实现,这种情况可以选用可配置的方式来实现。
■ 不方便扩展子工厂
私有化简单工厂的构造方法,使用静态方法来创建接口,也就不能通过写简单工厂类的子类来改变创建接口的方法的行为了。不过,通常情况下是不需要为简单工厂创建子类的。
2.3.6 思考简单工厂
1.简单工厂的本质
简单工厂的本质是:选择实现。
注意简单工厂的重点在选择,实现是已经做好了的。就算实现再简单,也要由具体的实现类来实现,而不是在简单工厂里面来实现。简单工厂的目的在于为客户端来选择相应的实现,从而使得客户端和实现之间解耦。这样一来,具体实现发生了变化,就不用变动客户端了,这个变化会被简单工厂吸收和屏蔽掉。
实现简单工厂的难点就在于“如何选择”实现,前面讲到了几种传递参数的方法,那都是静态的参数,还可以实现成为动态的参数。比如,在运行期间,由工厂去读取某个内存的值,或者是去读取数据库中的值,然后根据这个值来选择具体的实现等。
2.何时选用简单工厂
建议在以下情况中选用简单工厂。
■ 如果想要完全封装隔离具体实现,让外部只能通过接口来操作封装体,那么可以选用简单工厂,让客户端通过工厂来获取相应的接口,而无须关心具体的实现。
■ 如果想要把对外创建对象的职责集中管理和控制,可以选用简单工厂,一个简单工厂可以创建很多的、不相关的对象,可以把对外创建对象的职责集中到一个简单工厂来,从而实现集中管理和控制。
2.3.7 相关模式
■ 简单工厂和抽象工厂模式
简单工厂是用来选择实现的,可以选择任意接口的实现。一个简单工厂可以有多个用于选择并创建对象的方法,多个方法创建的对象可以有关系也可以没有关系。
抽象工厂模式是用来选择产品簇的实现的,也就是说一般抽象工厂里面有多个用于选择并创建对象的方法,但是这些方法所创建的对象之间通常是有关系的,这些被创建的对象通常是构成一个产品簇所需要的部件对象。
所以从某种意义上来说,简单工厂和抽象工厂是类似的,如果抽象工厂退化成为只有一个实现,不分层次,那么就相当于简单工厂了。
■ 简单工厂和工厂方法模式
简单工厂和工厂方法模式也是非常类似的。
工厂方法的本质也是用来选择实现的,跟简单工厂的区别在于工厂方法是把选择具体实现的功能延迟到子类去实现。
如果把工厂方法中选择的实现放到父类直接实现,那就等同于简单工厂。
■ 简单工厂和能创建对象实例的模式
简单工厂的本质是选择实现,所以它可以跟其他任何能够具体的创建对象实例的模式配合使用,比如:单例模式、原型模式、生成器模式等。