我们希望类工厂具有两项功能。第一,类工厂应该能够为开发者提供类实例,而无须开发者直接实例化类。在把对象送出类工厂之前,它就应该做好对象的“准备”工作。第二,类工厂应该能够使远程对象的实例化过程对开发者透明,并减少开发者使用远程对象时需要了解的远程配置。
GOF的“抽象工厂”模式为类工厂服务提供了优秀蓝图。首先让我们了解抽象工厂的方方面面,以及如何使用它构建达到上述两个目标的类工厂服务。
4.3.1 抽象工厂设计模式
GOF的书中说:“提供一个创建一系列相关或互相依赖对象的接口,而无须指定它们具体的类。”图4-5显示了该模式的结构。

图4-5 GOF抽象工厂设计模式的结构
抽象工厂允许开发者在代码中使用具体类,而无须引用具体类。开发者要做的就是,引用他们希望使用的具体类的抽象基类。抽象工厂免去了开发者调用new ConcreteClassXX()之苦。下面的例子说明了使用抽象工厂与不使用抽象工厂时代码的区别。
ConcreteProductA1 cpa1 = new ConcreteProductA1();
cpa1.ShowPrice();
// 使用抽象工厂
ProductA pa = ClassfactoryService.Getfactory1().CreateProductA();
pa.ShowPrice();
第一段代码中,我们必须引用具体类;如果我们决定建立一个不同的类来描述ProductA1,我们不得不把代码改成ConcreteNewProductA1 cnpa1 = new ConcreteNewProductA1(),并且还要在代码中替换所有ConcreteProductA1的引用。
第二段代码中,我们无须引用任何具体类。ProductA是一个抽象基类,供所有ProductA的子类家族继承。当我们需要得到一个ProductA1具体类的对象时,只须调用类工厂就可以了。CreateProductA()方法返回对象的类型为ProductA,尽管该对象实际上引用一个ConcreteProductA1对象。如果我们决定创建一个不同的类来描述
ProductA1,则只须修改类工厂中的CreateProductA1()方法就可以了;客户代码不用做任何修改,因为它只涉及抽象类ProductA。下面是CreateProductA1()方法的例子:
{
//ConcreteProductA1 p = new ConcreteProductA1();
ConcreteNewProductA1 p = new ConcreteNewProductA1();
return p;
}
藏在抽象工厂模式背后的思想是:将使用业务对象的客户代码和创建那些业务对象的类分离。在客户端,我们通过抽象父类或接口来访问业务对象,因而避免了对具体子类的直接引用。当具体子类发生改变时,我们只需对客户代码做很少改动,甚至无须改动。
| 注释:如果你在代码中要使用一些特定具体子类的方法或属性,但它们并不属于其抽象父类或接口,那么在访问这些子类层次资源之前,你必须将对象的类型转换成具体类。目前无法解决这种耦合。然而,如果你并未使用这种特定子类层次的方法或属性,应该尽量用抽象父类或接口引用对象,这样,当必须改变时,我们才能使改动的代码量最少。 |
4.3.2 类工厂服务的设计
当开发应用框架时,我们希望确保开发者不会把对象创建代码和具体类混在一起。类工厂服务为我们提供了这个功能。为框架开发类工厂与为客户化应用开发类工厂有所不同:对后一种情况,我们确切地知道有多少抽象类工厂,以及每个工厂要负责创建多少种具体类;而对前一种情况,即开发类工厂作为框架组件的情况下,我们并不知道在开发者构建应用时,需要多少工厂和类。但是,使用.NET反射机制来创建类工厂服务,可以使它足够灵活地支持加入应用的新类。图4-6显示了SAF.ClassFactory的结构,以及它与特定应用类工厂之间的关系。

图4-6 SAF.ClassFactory的结构
SAF.ClassFactory包含三个主要组件。第一个组件是ClassFactory类,它是一个具体类,具有一个声明为GetFactory(string factoryName)的静态方法。该方法负责在请求到来时,加载特定应用的类工厂具体类,并将之返回给调用者。在客户端请求一个特定应用类工厂的代码如下:
//Assign application-specific factory class to abstract class type
pf = (ProductFactory)SAF.ClassFactory.GetFactory("ProductFactory-A");
//Assign concrete class to abstract class type
Product cheapProduct = pf.GetCheapProduct();
Product.RaisePrice();
在本例中,ProductFactory和Product都是抽象类。尽管没有明确定义具体ConcreteProductA类和CheapProduct类,但当我们调用RaisePrice()方法时,CLR知道我们实际上是在调用ProductA家族的CheapProduct对象;该对象是从一个应用层次的类工厂中获得的,而该类工厂又是从SAF.ClassFactory服务中获得的。
我们希望达到的目标之一是,在整个应用中尽可能多地使用强类型对象的引用,而不是通用对象类型(generic object type)的引用。因为当我们在使用强类型对象时,编译器在编译时能查出任何一个类型不匹配的错误。而且用强类型对象编写代码时,开发者的效率会更高,因为IDE提供的智能感知(Intellisense)功能有助于他们确定要用的方法或属性。而一旦选择了把对象定义为Object类型,开发者就不得不在访问该对象的方法或属性之前,先把该对象强制转换成特定类型。这会使代码变得晦涩难懂,有时还要求开发者了解额外的类层次信息,而这些信息本应对开发者是不可见的。因此,我们应该在代码中尽量多地使用强类型对象。下面是一段使用弱类型(此即Object类型)对象的代码:
GetFactory()方法返回一个Object类型的类工厂,所以开发者就不得不在使用它们之前,将它们的类型转换为特定应用的抽象类类型。我们可以在SAF.ClassFactory中增加另外的方法来返回强类型对象。例如,我们可能增加一个GetProductFactory()方法返回ProductFactory类型,或者增加一个GetCustomerFactory()方法返回CustomerFactory类型。但是,这样做违背了特定应用信息与应用框架分离的原则:在框架组件中增加这种专有方法,会导致当增加新的类工厂或删除老的类工厂时,框架频繁地改变。这是因为,我们必须不停地增加、删除和修改SAF.ClassFactory类中的方法,以适应那些业务领域层次的变化。
应用框架应当足够灵活,以便将自己和业务领域的变化隔离开来。为此,我们有时不得不在框架代码中使用弱类型对象,以获得高级别的灵活性。然而,如果实在希望在应用中使用强类型对象,使开发者的生活(工作)轻松些,你完全可以在应用框架之上再封装一层。该附加层使用特定应用的强类型对象,在开发时使用该层而不要使用框架本身。通过这一附加的特定应用层,既能够将框架同业务领域变化隔离,又能够为开发者提供“强类型”环境。本章附带的测试项目演示了这一方法,TestConcreteFactory项目(它将调用SAF.ClassFactory)具有这一附加的特定应用层,其中包含了用于特定业务应用的具体类工厂。
现在让我们回到SAF.ClassFactory,这个服务在示例中用到了诸如ProductFactory-A等关键字,来标识要加载的特定类工厂。为此,SAF.ClassFactory使用了配置文件和.NET反射机制。配置文件中有一段包含了应用使用的所有特定应用类工厂信息,如下所示:
<Class name="ProductFactory-A"
type="TestConcreteFactory.ConcreteProductFactory,TestConcreteFactory"/>
<Class name="ProductFactory-B"
type="TestConcreteFactory.ConcreteNewProductFactory,TestConcreteFactory/>
<Class name="CustomerFactory-A"
type="TestConcreteFactory.ConcreteCustomerFactory, TestConcreteFactory" />
</SAF.ClassFactory>
SAF.ClassFactory能够从每个Class元素中获得正好够用的信息来加载和实例化类工厂。为了使用反射来创建类实例,我们至少需要一条信息:待创建实例所属类的类型信息。GetFactory()是一个静态方法,它使用配置文件中的上述信息,为每个请求动态加载类工厂。下面是GetFactory()方法的代码:
{
object factory = null;
ConfigurationManager cm =
(ConfigurationManager)ConfigurationSettings.GetConfig("Framework");
ClassFactoryConfiguration cf = cm.ClassFactoryConfig;
XmlNode classFactoryData = cf.GetFactoryData(factoryName);
//obtain the type information
string type = classFactoryData.Attributes["type"].Value;
Type t = System.Type.GetType(type);
factory = Activator.CreateInstance(t, null);
return factory;
}
在第6章讨论SAF.Configuration服务时,将解释ConfigurationManager是如何工作的。除此之外,只要你阅读了本章前面有关.NET反射的介绍,上面的代码就很容易读懂了。利用反射,我们可以通过改动配置文件中的信息来增加、修改和删除类工厂。
| 注释:我们或许不用在客户每次调用GetFactory()方法时,都重新创建一个类工厂。对象缓存技术可以帮助我们来重用类工厂。在第5章我们会通过SAF.Cache来了解更多对象缓存技术。 |
假设我们已经建立了一组具有不同实现和行为的具体产品类,以及一个产生新类实例的类工厂。我们只需简单地修改配置文件,当应用下一次重新加载时,所有针对老产品类的请求将应用于一组新产品类,而不必改动任何代码。下面的对比显示了使用SAF.ClassFactory框架组件时代码的改动量:
<Class name="ProductFactory-A" type="
TestConcreteFactory.ConcreteProductFactory,TestConcreteFactory"/>
To:
<Class name="ProductFactory-A" type="
TestConcreteFactory.ConcreteNewProductFactory,TestConcreteFactory"/>
创建一个新的特定应用类工厂,并使它和SAF.ClassFactory一起协作使用是很容易的。下面是ConcreteNewProductFactory类的骨架:
{
//methods that return various concrete objects.
public CheapProduct GetCheapProduct()
{
CheapProduct cp = new NewCheapProduct();
//perform possible action on cp before returning it
return cp;
}
public CheapProduct GetCheapProduct(string currency) {...};
public GoodProduct GetGoodProduct() {...};
}
如例中所示,你可以在每个方法中简单地用new关键字创建一个对象,然后返回该对象。但是,你还可以进行其他操作。例如,根据业务规则为新创建对象设置属性,
或是进行对象缓存等。只要保证每个方法返回一个对象引用给调用者,就可以在环境允许的情况下进行很多个额外操作。
不难理解,通过使用SAF.ClassFactory和多组特定应用的类工厂,我们可以把创建对象的业务逻辑同使用该对象的业务逻辑隔离开,从而使改变具体类的影响最小化。至此,我们已经完成了类工厂服务的第一个目标。接下来,看看如何让类工厂服务支持.NET remoting,方法有二:一个简单,另一个复杂。图4-7显示了两种方法的差异。

图4-7 两种类工厂方案的对比
其中复杂的方法需要你通过配置文件或编程手段,在服务器端和客户端注册远程对象。换言之,如果要为客户端产生远程对象,你必须为每个具体类工厂进行大量远程对象注册工作。因为类工厂是和客户代码完全独立的组件,所以你可以在每个具体类工厂中,做支持远程对象的任何事情,而不会影响客户代码。这是一个有效的办法,但代价是额外的配置和编码工作。另外一个方法却简单得多,而且也更容易实现。
在介绍这个支持类工厂中远程对象的简单方法之前,我们先考虑一些其他的事情。如果只是想创建一个远程对象,我们可以在配置中指明或通过编程注册这个远程对象,以告诉CLR要为远程对象获得一个代理,并让每一个调用都通过那个代理进行。我们本可以用Activator.GetObject()或Activator.CreateInstance()来避免在客户端注册远程对象,但为了支持remoting,在服务器端还是必须配置和注册那些远程对象。
然而,有没有办法减少让应用支持remoting的工作量呢?有,那就是让CLR来完成所有繁杂的工作。CLR有一个规则,当一个远程对象传送给客户时,CLR会自动对该远程对象进行列集(marshal)处理,并返回该对象的ObjRef对象代替它。本章前面已经介绍过ObjRef对象,它是一个可序列化对象,包含客户创建连接远程对象代理所需的所有信息。换言之,如果我们使特定应用类工厂本身变成了一个服务器端远程对象,那么远程类工厂将返回ObjRef对象给请求对象的客户端。图4-8说明了第二种方法的思想。

图4-8 .NET远程调用过程
一开始,客户应用向类工厂ConcreteProductFactory请求CheapProduct(图中步骤1)。因为ConcreteProductFactory位于远程计算机上,所以ClassFactory必须通过调用Activator.GetObject()获取远程类工厂。远程服务器上的CLR根据客户端的请求,创建一个具体类工厂(图中步骤2)。GetCheapProduct()方法被调用,然后一个新的CheapProduct对象被返回(图中步骤3)。GetCheapProduct()方法向CLR返回该对象(这个对象对远程服务器来说是本地对象),然后CLR把该对象转发给位于另一台计算机上的客户应用(图中步骤4)。CLR知道请求该对象的客户程序在远程计算机上,所以它自动对该对象进行列集处理,创建该CheapProduct对象的ObjRef对象(图中步骤5),然后把ObjRef对象返回给客户端(图中步骤6)。重要的是,我们不仅要使工厂类是可远程化的(remotable),而且还要使工厂类产生的类也是可远程化的。为此,我们必须确保它们都从MarshalByRefObject类继承。客户端的CLR截获ObjRef对象,并自动将其散集处理成为代理对象(确切地说是透明代理),然后把代理对象赋值给变量p(图中步骤7)。下一次客户应用访问p时,实际上是和代理进行交互,这个代理知道如何把方法调用转换为消息,并将消息发送到正确的目的地(这一过程在本章前面.NET remoting概览部分讨论过)。
这个方法的关键在于,把具体的特定应用类工厂部署到具体业务类所在的远程服务器上,并让远程计算机中的CLR为客户应用请求的业务类自动返回一个ObjRef对象。
为使SAF.ClassFactory支持远程具体类工厂,我们必须修改一下GetFactory()方法和配置文件。下面是要对配置文件做修改的例子:
<Class name=" ProductFactory-A" type="
TestConcreteFactory.ConcreteProductFactory,TestConcreteFactory" />
To:
<Factory name=" ProductFactory-A" type="
TestConcreteFactory.ConcreteProductFactory,TestConcreteFactory "
location="http://RemoteServer1/ClassFactory.rem" />
我们还需要用Activator来创建一个远程类工厂的代理。下面是GetFactory()方法的新实现:
{
object factory = null;
ConfigurationManager cm =
(ConfigurationManager)ConfigurationSettings.GetConfig("Framework");
ClassFactoryConfiguration cf= cm.ClassFactoryConfig;
XmlNode classFactoryData = cf.GetFactoryData(factoryName);
//obtain the type information
string type = classFactoryData.Attributes["type"].Value;
Type t = System.Type.GetType(type);
//create an instance of concrete class factory
if (classFactoryData.Attributes["location"] != null)
{
string location = classFactoryData.Attributes["location"].Value;
factory = Activator.GetObject(t,location);
}
else
{
factory = Activator.CreateInstance(t,null);
}
return factory;
}
没有必要在客户端做额外的remoting配置。但是,我们需要在远程服务器上注册每一个类工厂。因为无须为每一个类工厂建立多个实例,所以我们应该使用单体(Singleton)作为服务器激活模式。服务器端仅有的remoting配置是为类工厂设置的,而不是为业务类设置的。利用CLR自动把本地对象引用转换为列集处理过(marshaled)的ObjRef对象,避免了将类工厂产生的每一个具体业务类注册成远程对象的麻烦。
| 注释:SAF.ClassFactory仅支持客户端激活的远程对象。也就是说,当客户从远程类工厂中收到一个远程对象时,这个远程对象是客户端激活的,这意味着客户可以对这个远程对象做多次调用。 |
还有一件事值得一提,即开发期间对客户应用中类引用的设置。因为类工厂和具体业务类现在都在远程,所以开发者必须首先获得它们的类型信息才能在代码中调用它们。为使客户应用得到类型信息,可以选择下面三种方案之一:第一种方案是把所有远程对象的接口程序集(interface assembly)部署到客户端;第二种方案是把实际的具体远程对象部署到客户端;第三种方案是在客户端部署一组“空”类,这组类有着和远程对象一样的类型信息和方法签名。
第一种方案的具体实现是,创建一组包含所有方法签名的远程接口类,并在远程具体类工厂和远程业务类中实现它们。之后,你可以把这组远程接口编译成一个程序集文件(assembly file)发送给开发者,并要求他们把它加入到项目引用中,这样客户应用就获得了调用远程工厂对象和业务对象所需的类型信息。这个方法有两个主要缺点:首先是客户应用无法访问对象的公共字段(public field),这是因为公共字段不能放入接口。第二个缺点有些严重。当你试图以其他远程接口对象为参数调用远程方法时,会收到一个运行时错误。这个错误是CLR为了将参数列集(marshal)成ObjRef对象,而调用参数的基类的CreateObjRef()方法时产生的。这是因为参数对象只是一个远程对象的接口,它并没有继承MarshalByRefObject,自然也就没有CreateObjRef()方法,从而CLR不能成功地列集该参数。
第二种方案是在客户端部署实际的远程类。它解决了第一种方案的问题,但是它却违背了面向对象编程中封装或信息隐藏的原则。该方案还有其他一些缺点(除了它使你感觉很差之外),但都不严重。实际上,微软已经在.NET文档的无数个remoting例子中使用了该方案。对已交付的实现类或DLL文件,可以用ildasm.exe了解其源代码中有些什么。还有另外几个工具可以把IL代码反编译为 C#或VB.NET代码,这使你的程序存在很大安全隐患,其中最重要的就是程序中的bug可能被别人发现。该方案的另一个风险是,你必须保证remoting配置是正确的,否则应用可能会去调用本地版的类实现,而不是网络另一端的远程对象,这可能是很难察觉的问题。
考虑到前面两种方案的缺点,我推荐把远程对象类型信息部署到客户端的第三种方案。SoapSuds.exe是.NET Framework SDK提供的一个工具,可以用于该方案。这个工具支持你自动生成一个“空”类,其中仅包含远程对象的类定义。然后,SoapSuds.exe会把这些空类放入一个DLL程序集(DLL assembly),或者一个随后再编译成程序集的源文件。因为SoapSuds.exe创建的是一个从MarshalByRefObject继承而来的具体“空”类,所以我们不用担心CLR会因为接口类型参数而报运行时错误,并且客户应用现在可以访问所有类的公共成员了。因为在这些类中没有方法的实现代码,所以避免了“有缺陷的代码”被陌生人看到的危险。SoapSuds.exe位于“\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Bin”文件夹中,它非常易用。例如:
-oa:ProductFactory.dll
ProductFactory.dll是输出文件。你可以用Visual Studio.NET的“增加引用(add reference)”功能将它加入客户应用的引用中。我们使用了“-nowp”开关(switch),这很重要,它的意思是“没有包装程序代理(no wrapper proxy)”。如果不用这个开关,SoapSuds就会创建一个通过SOAP执行的远程调用代理类。这不是我们想要的。我们想要的仅仅是一个“空”类定义,这样客户端代码可以编译它。那些创建代理对象的工作留给了CLR,它会在接收到来自远程服务器的ObjRef对象时完成这些工作。





