了解模式需要研究客户端程序(使用模式者)和模式的内部结构,并且理解两者之间的通信接口、模式本身因功能增加造成的变动(易碎性),以及客户端程序利用新功能的难易程度。本文针对这些问题提出了一个具体的抽象工厂模式实现方案。
模式结构介绍
这个模式由一个工厂类层次和N个产品类层次组成。从每一个产品类层次中取出一个产品类形成产品类族,这个类族的实例为产品族。产品族中的产品之间有一种依赖关系。一个具体的工厂类负责创建产品族中的各个产品。
从图1可以看出,通信接口由一个抽象工厂接口和两个抽象产品组成。模式部分显示了两个产品类 ProductA 和ProductB、两个产品类族ProductA1、ProductB1和ProductA2、ProductB2,以及两个产品类族对应的两个工厂ConcreteFactory1、ConcreteFactory2。
图1 标准抽象工厂模式
从模式定义中知道这个模式的意图内容为:
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。工厂类层次的通信接口只有抽象工厂和创建产品族的各个工厂方法,这些工厂方法不带任何参数,并且返回具有抽象产品类型的具体产品实例。这些使得客户端可以不依赖具体产品的类,从而体现了模式的意图。意图中的“而无需指定它们具体的类”可以理解为客户端在使用和创建具体产品时不给出具体产品的任何暗示。
变动
我们分析模式的变动时必须先固化模式和客户端间的通信接口,即通信接口是固定不变的。分析变动就是分析模式的易碎性(模式实现适应变动的能力)。对抽象工厂模式可以考虑两个变动:
1. 加一个产品类族;
2. 加一个产品类。
变动一的易碎性较小,我们只需在每个产品类层次中增加这个产品族中产品类作为抽象产品类的子类,同时增加一个工厂子类来创建这个产品类族的产品族足以。
变动二要求在模式中增加一个产品类,意味着增加一个产品类层次。由于通信接口中的工厂方法固定了客户能创建的产品的类型数目,所以增加一个产品类意味着修改与客户端的通信接口,进一步意味着旧的客户端要使用新的模式实现就要重新编码,整个工厂类层次都需要改动。综合上述,这个变动的易碎性很大。
设计
程序架构
一个程序可以从三个维度进行架构,这三个维度是层、级和服务。层代表了不同的抽象程度,比如操作系统的分层;级代表不同的角色和职责,比如Client/Server的两级模型;服务代表了具有通用功能的支持模块。
实现一个模式从级这个维度来讲可以简单地分成两级,即客户端和模式端。从服务维度讲模式可以利用某些服务,如事务、名字和目录服务、安全等服务器提供商开发的设施服务。模式本身也可以实现成供客户端访问的服务,从而形成应用服务,典型的有EJB组件。应用程序的开发从层上考虑的比较少。图2体现了实现抽象工厂模式的程序架构。
图2 程序架构
JNDI
JNDI是Java Naming and Directory Interface的简写,即Java命名和目录服务接口。这个接口是名字和目录服务通用编程的API。
命名服务是一个系统基础设施,给对象绑定一个名字,并能通过名字找到对象的机制。这个名字一般是面向使用者的。类似的服务实现有因特网域名系统(实现通过域名www. attern.com找到IP地址的手段)、文件系统(实现通过文件名找到文件的手段)等。
目录服务是名字服务的扩展,除了提供名字绑定之外,还允许对象拥有属性。目录服务中的对象为目录对象。图3和图4分别表示了名字服务和目录服务的概念定义。
图3 名字服务的概念
图4 目录服务的概念
一个名字系统由相互联系的一系列上下文组成,上下文是名字与对象的绑定集合,相互联系的一组上下文能形成一个层次结构。一个名字系统中所有的名字组成了这个系统的命名空间,而且命名空间有它自己的命名规范。一个目录库由一系列目录对象组成,每个目录对象可以有若干个属性相连。
1.JNDI架构如图5,其中JNDI SPI为服务提供者接口,LDAP、DNS、NIS等为服务提供者,JNDI API为客户程序使用服务的编程接口。
图5 JNDI架构
2.JNDI Java包,JDK1.3和后续版本已经包含了JNDI,另外还有几个服务提供者,如LDAP、COS和RMI。其他的服务提供者可以从http://java.sun.com/products/jndi/serviceproviders.html下载。JNDI分为五个包javax.naming 、javax.naming.directory 、javax.naming.event、 javax.naming.ldap 和javax.naming.spi ,一般情况下只需要前三个包。名字和目录操作都是针对一个上下文来说的,但是没有绝对根的上下文,所以就用一个初始上下文 InitialContext作为名字和目录操作的起点,一旦有了这个上下文,就可以用它来查找其它上下文和对象。下面是进行名字操作和目录操作初始上下文类的关系:
3.使用JNDI,在模式的实现中,我们可以用名字服务来实现子类的配置,也就是说在名字服务中指定子类的名字,由模式读取这个子类配置,并创建一个子类实例返回给客户端,客户端用抽象父类来返回子类实例。通过这种机制实现“针对接口编程,而不是实现”的重用的面向对象设计的原则。
Singlton模式的实现可以利用JNDI名字服务来实现。在名字服务中存放Singlton模式中的惟一实例,使用者要使用这个实例可以使用JNDI编程接口查询这个对象。我们的工厂类对象的产生就可以采用这个方法。
类图设计
目前没有较好的办法解决增加产品类带来的旧客户程序不能透明地使用新产品类的方案。如果能忍受这一点,这个变动的其它影响还是可以解决的。图6为一个基于Class第一类对象的Java抽象工厂的模式变体结构图。由此可以看出,抽象工厂不仅是接口,还是具体完成创建工作的类。它只有一个工厂方法,以抽象产品的类名为参数,以Java类库的最顶层类Object为返回值。客户端把通信接口抽象产品类名传给这个工厂方法,接着通过一个强制类型转换而得到抽象产品对象。命名服务实现产品类族中每个产品类名到具体产品类名的映射,通过这样一系列的映射定义了每一个具体工厂类要创建的产品类族。
图6 抽象工厂模式变体类结构图
图6中还显示了编写单元测试用例的类UnitTest和实用类SerObj。
实现工厂通信接口的设计
1.客户端代码如下:
Factory factory = Factory. getInstance
( "ldap://localhost:389/ dc = pattern, dc = com" ) ;
AbstractProductA productA = factory. CreateProduct
( AbstractProductA的完全类名 );
名字服务包含Factory的名字到工厂类对象的绑定。整个机制如图7。
图7 工厂类的Singleton设计
2.单元测试代码如下:
3.实现工厂Singlton模式的代码如下:
4.具体的工厂方法代码如下:
public class Factory implements java.io.Serializable {
...
/**
接受抽象产品类的完全类名,查询目录服务得到具体产品类的完全类名,采用JAVA的CLASS为第一类对象的机制创建相应类的对象。
*/
public Object createProduct(String vstrClassName ){
Context ctx = null;
String strEntryName;
String strEntryClassName;
Object oResult = null;
try{
ctx = getInitialContext ( ) ;
strEntryName = "cn=" + vstrClassName;
strEntryClassName = ( String )ctx.lookup ( strEntryName ) ;
try {
Class clTem;
clTem = Class.forName ( strEntryClassName ) ;
oResult = clTem.newInstance ( ) ;
}
catch ( Exception ex ) {
logger.error ( ex.toString ( ) );
}
}
catch ( NamingException e ) { }
finally{
try{
ctx.close ( ) ;
}
catch ( Exception e1 ) { }
}
return oResult;
}
...
}
实现产品通信接口的设计
抽象工厂模式的本意要求我们创建具体产品对象时,客户端不能暗示任何具体产品对象类型的信息。但是通过分析模式的通信接口可知,客户端可以告诉工厂类这些具体产品的父类。关于模式提到创建一些互相依赖的对象的本意,我们可以在目录服务的目录库中实现。图8、图9分别定义了这样的产品族关系。
图8 目录服务产品族1的配置
图9 目录服务产品族2的配置
1.图8表示在工厂类的工厂方法要求创建产品族1时,可以在目录服务中指定抽象产品的完全类名绑定到产品族1的相应类名上。
(1)单元测试代码如下:
public class UnitTest extends TestCase{
...
public void testProduct1 ( ) {