7.3 模 式 讲 解
7.3.1 认识抽象工厂模式
1.抽象工厂模式的功能
抽象工厂的功能是为一系列相关对象或相互依赖的对象创建一个接口。一定要注意,这个接口内的方法不是任意堆砌的,而是一系列相关或相互依赖的方法,比如上面例子中的CPU和主板,都是为了组装一台电脑的相关对象。
从某种意义上看,抽象工厂其实是一个产品系列,或者是产品簇。上面例子中的抽象工厂就可以看成是电脑簇,每个不同的装机方案,代表一种具体的电脑系列。
2.实现成接口
AbstractFactory在Java中通常实现成为接口,大家不要被名称误导了,以为是实现成为抽象类。当然,如果需要为这个产品簇提供公共的功能,也不是不可以把AbstractFactory实现成为抽象类,但一般不这么做。
3.使用工厂方法
AbstractFactory定义了创建产品所需要的接口,具体的实现是在实现类里面,通常在实现类里面就需要选择多种更具体的实现。所以AbstractFactory定义的创建产品的方法可以看成是工厂方法,而这些工厂方法的具体实现就延迟到了具体的工厂里面。也就是说使用工厂方法来实现抽象工厂。
4.切换产品簇
由于抽象工厂定义的一系列对象通常是相关或者相互依赖的,这些产品对象就构成了一个产品簇,也就是抽象工厂定义了一个产品簇。
这就带来非常大的灵活性,切换一个产品簇的时候,只要提供不同的抽象工厂实现就可以了,也就是说现在是以产品簇作为一个整体被切换。
5.抽象工厂模式的调用顺序示意图
抽象工厂模式的调用顺序如图7.3所示。
7.3.2 定义可扩展的工厂
在前面的示例中,抽象工厂为每一种它能创建的产品对象定义了相应的方法,比如创建CPU的方法和创建主板的方法等。
这种实现有一个麻烦,就是如果在产品簇中要新增加一种产品,比如现在要求抽象工厂除了能够创建CPU和主板外,还要能够创建内存对象,那么就需要在抽象工厂里面添加创建内存的一个方法。当抽象工厂一发生变化,所有的具体工厂实现都要发生变化,如下、如此就非常的不灵活。
现在有一种相对灵活,但不太安全的改进方式可以解决这个问题,思路如下:抽象工厂里面不需要定义那么多方法,定义一个方法就可以了,给这个方法设置一个参数,通过这个参数来判断具体创建什么产品对象;由于只有一个方法,在返回类型上就不能是具体的某个产品类型了,只能是所有的产品对象都继承或者实现的这么一个类型,比如让所有的产品都实现某个接口,或者干脆使用Object类型。
还是通过代码来体会一下,把前面那个示例改造成可扩展的工厂实现。
(1)先来改造抽象工厂。示例代码如下:
(2)CPU的接口和实现。主板的接口和实现和前面的示例一样,这里就不再示范了。CPU分成Intel的CPU和AMD的CPU,主板分成技嘉的主板和微星的主板。
注意
这里要特别注意传入createProduct的参数所代表的含义,这个参数只是用来标识现在是在创建什么类型的产品,比如标识现在是创建CPU还是创建主板。一般这个type的含义到此就结束了,不再进一步表示具体是什么样的CPU或具体是什么样的主板。也就是说type不再表示具体是创建Intel的CPU还是创建AMD的CPU,这就是一个参数所代表的含义的深度问题。要注意,虽然也可以延伸参数的含义到具体的实现上,但这不是可扩展工厂这种设计方式的本意,一般也不这么去做。
(3)下面来提供具体的工厂实现,也就是相当于以前的装机方案。
先改造原来的方案一吧,现在的实现会有较大的变化。示例代码如下:
用同样的方式来改造原来的方案二。示例代码如下:
(4)在这个时候使用抽象工厂的客户端实现,也就是在装机工程师类里面,通过抽象工厂来获取相应的配件产品对象。示例代码如下:
通过上面的示例,能看到可扩展工厂的基本实现。从客户端的代码会发现,为什么说这种方式是不太安全的呢?
仔细查看上面蓝色的代码,会发现什么?
你会发现创建产品对象返回来后,需要造型成为具体的对象,因为返回的是Object,如果这个时候没有匹配上,比如返回的不是CPU对象,但是要强制造型成为CPU,那么就会发生错误,因此这种实现方式的一个潜在缺点就是不太安全。
(5)下面来体会一下这种方式的灵活性。
假如现在要加入一个新的产品——内存,当然可以提供一个新的装机方案来使用它,这样已有的代码就不需要变化了。
内存接口的示例代码如下:
提供一个现代内存的基本实现。示例代码如下:
现在若要使用这个新加入的产品,以前实现的代码都不用变化,只需新添加一个方案,在这个方案里面使用新的产品,然后客户端使用这个新的方案即可。示例代码如下:
这个时候的装机工程师类,如果要创建带内存的电脑,需要在装机工程师类里面添加对内存的使用。示例代码如下:
可能有朋友会发现上面蓝色的代码中内存操作的地方,跟前面CPU和主板的操作方式不一样,多了一个if判断。原因是为了要同时满足以前和现在的要求,如果是以前的客户端,它调用的时候就没有内存,这个时候操作内存就会出错,因此添加一个判断,有内存的时候才操作内存,就不会出错了。
此时的客户端,只要选择使用方案三就可以了。示例代码如下:
运行结果如下:
测试一下看看,体会一下这种设计方式的灵活性。当然前面也讲到了,这种方式可能会不太安全,至于是否使用,就看具体应用设计上的权衡了。
7.3.3 抽象工厂模式和DAO
1.什么是DAO
DAO:数据访问对象,是Data Access Object首字母的简写。
DAO是JEE(也称JavaEE,原J2EE)中的一个标准模式,通过它来解决访问数据对象所面临的一系列问题,比如,数据源不同、存储类型不同、访问方式不同、供应商不同、版本不同等,这些不同会造成访问数据的实现上差别很大。
■ 数据源的不同,比如存放于数据库的数据源,存放于LDAP(轻型目录访问协议)的数据源;又比如存放于本地的数据源和远程服务器上的数据源等。
■ 存储类型的不同,比如关系型数据库(RDBMS)、面向对象数据库(ODBMS)、纯文件、XML等。
■ 访问方式的不同,比如访问关系型数据库,可以用JDBC、EntityBean、JPA等来实现,当然也可以采用一些流行的框架,如Hibernate、IBatis等。
■ 供应商的不同,比如关系型数据库,流行的如Orace1、DB2、SqlServer、MySQL等等,它们的供应商是不同的。
■ 版本不同,比如关系型数据库,不同的版本,实现的功能是有差异的,就算是对标准的SQL的支持,也是有差异的。
但是对于需要进行数据访问的逻辑层而言,它可不想面对这么多不同,也不想处理这么多差异,它希望能以一个统一的方式来访问数据。
此时系统结构如图7.4所示。
图7.4 业务对象访问DAO的示意图
也就是说,DAO需要抽象和封装所有对数据的访问,DAO承担和数据仓库交互的职责,这也意味着,访问数据所面临的所有问题,都需要DAO在内部来自行解决。
2.DAO和抽象工厂的关系
了解了什么是DAO后,可能有些朋友会想,DAO同抽象工厂模式有什么关系呢?看起来好像是完全不靠边啊。
事实上,在实现DAO模式的时候,最常见的实现策略就是使用工厂的策略,而且多是通过抽象工厂模式来实现,当然在使用抽象工厂模式来实现的时候,可以结合工厂方法模式。因此DAO模式和抽象工厂模式有很大的联系。
3.DAO模式的工厂实现策略
下面就来看看DAO模式实现的时候是如何采用工厂方法和抽象工厂的。
1)采用工厂方法模式
假如现在在一个订单处理的模块里面。大家都知道,订单通常分成两个部分,一部分是订单主记录或者是订单主表,另一部分是订单明细记录或者是订单子表,那么现在业务对象需要操作订单的主记录,也需要操作订单的子记录。
如果这个时候的业务比较简单,而且对数据的操作是固定的,比如就是操作数据库,不管订单的业务如何变化,底层数据存储都是一样的,那么这种情况下,可以采用工厂方法模式,此时系统结构如图7.5所示。
图7.5 DAO模式的工厂方法实现策略结构示意图
从上面的结构示意图可以看出,如果底层存储固定的时候,DAOFactory就相当于工厂方法模式中的Creator,在里面定义两个工厂方法,分别创建订单主记录的DAO对象和创建订单子记录的DAO对象,因为固定是数据库实现,因此提供一个具体的工厂RdbDAOFactory(Rdb,关系型数据库)来实现对象的创建。也就是说DAO可以采用工厂方法模式来实现。
采用工厂方法模式的情况,要求DAO底层存储实现方式是固定的,这种模式多用在一些简单的小项目的开发上。
2)采用抽象工厂模式
实际上更多的时候DAO底层存储实现方式是不固定的,DAO通常会支持多种存储实现方式,具体使用哪一种存储方式可能是由应用动态决定,或者是通过配置来指定。这种情况多见于产品开发,或者是稍复杂的应用、亦或较大的项目中。
对于底层存储方式不固定的时候,一般采用抽象工厂模式来实现DAO。比如现在的实现除了RDB的实现,还会有Xml的实现,它们会被应用动态的选择,此时系统结构如图7.6所示。
图7.6 DAO模式的抽象工厂实现策略结构示意图
从上面的结构示意图可以看出,采用抽象工厂模式来实现DAO的时候,DAOFactory就相当于抽象工厂,里面定义一系列创建相关对象的方法,分别是创建订单主记录的DAO对象和创建订单子记录的DAO对象,此时OrderMainDAO和OrderDetailDAO就相当于被创建的产品,RdbDAOFactory和XmlDAOFactory就相当于抽象工厂的具体实现,在它们里面会选择相应的具体的产品实现来创建对象。
4.代码示例使用抽象工厂实现DAO模式
(1)先看看抽象工厂的代码实现。示例代码如下:
(2)看看产品对象的接口,就是订单主、子记录的DAO定义。
先来看看订单主记录的DAO定义。示例代码如下:
再来看看订单子记录的DAO定义。示例代码如下:
(3)接下来实现订单主、子记录的DAO。
先来看看关系型数据库的实现方式,示例代码如下:
Xml实现的方式一样。为了演示简单,都是输出了一句话。示例代码如下:
(4)再看看具体的工厂实现。
先来看看关系型数据库实现方式的工厂。示例代码如下:
Xml实现方式的工厂的示例代码如下:
(5)好了,使用抽象工厂简单地实现了DAO模式。在客户端通常是由业务对象来调用DAO,那么该怎么使用这个DAO呢?示例代码如下:
通过上面的示例,可以看出DAO可以采用抽象工厂模式来实现,这也是大部分DAO实现所采用的方式。
7.3.4 抽象工厂模式的优缺点
抽象工厂模式的优点
■ 分离接口和实现
客户端使用抽象工厂来创建需要的对象,而客户端根本就不知道具体的实现是谁,客户端只是面向产品的接口编程而已。也就是说,客户端从具体的产品实现中解耦。
■ 使得切换产品簇变得容易
因为一个具体的工厂实现代表的是一个产品簇,比如上面例子的Scheme1代表装机方案一:Intel的CPU+技嘉的主板,如果要切换成为Scheme2,那就变成了装机方案二:AMD的CPU+微星的主板。
客户端选用不同的工厂实现,就相当于是在切换不同的产品簇。
抽象工厂模式的缺点
■ 不太容易扩展新的产品
前面也提到这个问题了,如果需要给整个产品簇添加一个新的产品,那么就需要修改抽象工厂,这样就会导致修改所有的工厂实现类。在前面提供了一个可以扩展工厂的方式来解决这个问题,但是又不够安全。如何选择,则要根据实际应用来权衡。
■ 容易造成类层次复杂
在使用抽象工厂模式的时候,如果需要选择的层次过多,那么会造成整个类层次变得复杂。
举个例子来说,就比如前面讲到的DAO的示例,现在这个DAO只有一个选择的层次,也就是选择是使用关系型数据库来实现,还是用Xml来实现。现在考虑这样一种情况,如果关系型数据库实现里面又分成几种,比如,基于Oracle的实现、基于SqlServer的实现、基于MySql的实现等。
那么客户端怎么选择呢?不会把所有可能的实现情况全部都做到一个层次上吧,这个时候客户端就需要一层一层地选择,也就是整个抽象工厂的实现也需要分出层次来,每一层负责一种选择,也就是一层屏蔽一种变化,这样很容易造成复杂的类层次结构。
7.3.5 思考抽象工厂模式
抽象工厂模式的本质:选择产品簇的实现。
1.抽象工厂模式的本质
工厂方法是选择单个产品的实现,虽然一个类里面可以有多个工厂方法,但是这些方法之间一般是没有联系的,即使看起来像有联系。
但是抽象工厂着重的就是为一个产品簇选择实现,定义在抽象工厂里面的方法通常是有联系的,它们都是产品的某一部分或者是相互依赖的。如果抽象工厂里面只定义一个方法,直接创建产品,那么就退化成为工厂方法了。
2.何时选用抽象工厂模式
建议在以下情况中选用抽象工厂模式。
■ 如果希望一个系统独立于它的产品的创建、组合和表示的时候。换句话说,希望一个系统只是知道产品的接口,而不关心实现的时候。
■ 如果一个系统要由多个产品系列中的一个来配置的时候。换句话说,就是可以动态地切换产品簇的时候。
■ 如果要强调一系列相关产品的接口,以便联合使用它们的时候。
7.3.6 相关模式
■ 抽象工厂模式和工厂方法模式
这两个模式既有区别,又有联系,可以组合使用。
工厂方法模式一般是针对单独的产品对象的创建,而抽象工厂模式注重产品簇对象的创建,这是它们的区别。
如果把抽象工厂创建的产品簇简化,这个产品簇就只有一个产品,那么这个时候的抽象工厂跟工厂方法是差不多的,也就是抽象工厂可以退化成工厂方法,而工厂方法又可以退化成简单工厂,这也是它们的联系。
在抽象工厂的实现中,还可以使用工厂方法来提供抽象工厂的具体实现,也就是说它们可以组合使用。
■ 抽象工厂模式和单例模式
这两个模式可以组合使用。
在抽象工厂模式里面,具体的工厂实现,在整个应用中,通常一个产品系列只需要一个实例就可以了,因此可以把具体的工厂实现成为单例。