原文:
http://msdn.microsoft.com/msdnmag/issues/05/09/DesignPatterns/default.aspx#contents#contents
依赖注入
今天比以往更加注重对现有组件的重用和把异构组件联结成一种粘合框架。但是这种联结很快就成了一项让人畏缩的任务,因为这个时候程序的尺寸和复杂度都在增加,依赖性也是。减少这种依赖性扩展的一个方法就是使用依赖注入(Dependency Injection),它允许你把对象注入一个类,这胜于依赖这个类来建立自己的对象。
使用工厂类是实现依赖注入(DI)的通常方法。当一个组件创建了另一个类的一个private实例,它在组件内部使初始化逻辑内在化。初始化逻辑很少在创建组件的外部被重用,因此必需为其它需要该被创建的类的实例的类重写初始化逻辑。例如类Foo创建类Bar的一个实例,且类Bar的实例需要几个初始化步骤,这对每个Bar的实例是不同的,其它想要创建Bar的实例的类不得不重写在类Foo中能发现的相同的初始化逻辑。
开发者喜欢自动化那些单调而泛味的任务,然而许多开发者仍然手动完成那些如对象构造和依赖分解的方法,依赖分解能够描述成对一个类型或对象的已定义的依赖性的分解。另外,依赖注入,目的就是减少你必需写的样板联结和底层代码的数量
容器提供了一个抽象层可以把组件储存在其中,特别是DI容器通过提供一些实例化类的实例的通用工厂类减少我刚描述的依赖联结的种类,允许在更广的水平上重用构造逻辑。
在进入DI容器之前,先让我们回顾下贯穿整个DI容器的核心模式,抽象工厂模式。
工厂模式复习资料
在 Design Patterns (Addison-Wesley, 1995), 作者是Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides,描述了抽象工厂模式的意图:"为了构造和实例化一组相关的对象而不需要指定它们的具体的对象。"在你的程序中使用抽象工厂模式允许你定义抽象类来创建对象系列。通过封装实例和构造逻辑,你保留对依赖性和你的对象允许的状态的控制。
常常,因为某些确定的依赖性或者其它需求,对象需要以一种协调的方式被实例化。例如,当在客户端代码中创建System.Xml.XmlValidatingReader的一个实例时,当验证该XmlValidtingReader对象时,频繁地把一个XmlSchemaCollection对象与相关的schemas驻留在一起。
工厂模式的另一种类型称之为工厂方法。工厂方法是简单的一个方法,通常定义为静态,它惟一的目的就是返回一个类的实例。有时,在更利于多态的情况下,为了指出返回确定的接口实现或者子类,一个标记将被传人工厂方法。例如WebRequest的创建方法接收一个字符串或者Uri实例,并返回一个派生自WebRequest的子类的一个新的实例。
从这以后,我将简单地使用"工厂"既表示抽象工厂模式又表示工厂方法实现。
利用工厂实现DI
工厂允许程序把对象和组件联结在一起,而不需要暴露太多关于组件如何组合或者每个组件也许有些什么依赖性。代替散布在整个程序中复杂的创建代码,工厂允许把那些代码驻留在中心位置,因此有利于在整个程序中重用,然后客户端代码调用工厂中的创建方法,随后工厂返回那些被请求的类的完整的实例。封装被保护,且客户端有效地消除与任何种被要求创建和配置对象实例的管线的耦合性(这句不是很好翻译,请高手指点一下,原文如下:Encapsulation is preserved, and the client is effectively decoupled from any sort of plumbing required to create or configure the object instance.)。
Figure 1 Factory Functions
工厂所能做的,远不仅仅是简单地创建对象和聚合它们的依赖性。它们也能作为一个中央配置区域来跨越一个对象的所有实例统一应用服务和约束(见图1)。例如,一个工厂能返回实际对象实例的一个代理,代替返回一个对象的实例,因此让分布式方法调用成为可能。既然客户端程序并不知道正在处理的对象实例,实际上是一个代理,而不是对象的真正实例,那么,就根本不需要更改客户端的代码。这种服务类型的例子可以在.NET远程框架中发现。使用一个.NET配置文件,分布式对象能被显示地配置,且客户端程序使用"new"很简单就创建一个类的实例,这对本地和分布式对象都是相同的,也不分是客户端激活还是服务器断激活对象。所有这些配置和管理的发生不需要客户端了解.NET远程。
然而,工厂并不是没有缺点。大部分时间里,在某个程序中工厂实现是非常有价值的,却不能跨越其它程序重用的,常常,所有可用的创建选项在工厂实现中被硬编码,使得工厂自己没有扩展性。大部分时间,类调用工厂中的创建方法必需知道创建工厂的哪个子类。
其次,在编译时,一个使用工厂创建的对象所有的依赖性对于工厂来说是清楚的。暂时忘记一会儿那不相干的.NET反射,在运行时,没有办法在那些被创建的对象中插入或者改变行为方式和那些被装配的依赖性。所有这些必需发生在设计时,或者至少要求重新编译。例如假设,一个工厂创建了类Foo的实例且类Foo的实例需要类Bar的一个实例,那么,工厂就必需知道如何重新得到类Bar的一个实例。工厂必需创建一个新的实例,或者即使是产生对另一个工厂的调用。
第三,既然工厂是自定义每个单独的实现,在一个特殊的工厂中有一个重要的被控制的横切架构的级别(这句也太难翻译,请高手指点:there can be a significant level of cross-cutting infrastructure that is held captive inside a particular factory)。举一个这样的例子,一个工厂动态为一个实际对象替换代理对象。这个就是一部分架构的例子,既为了部署在一个分布式系统中简单对象的包装,那是完全被封装在一个特殊类中的。如果其它对象需要以一种相似的方式改变,这么做的逻辑被隐藏在工厂中,必需为其它对象重复所做的。一旦该功能在原有的程序之外需要,现在的问题变成了在既维持现有的工厂概念的同时如何重用这功能。
最后,工厂为了达到多态依赖于定义良好的接口。为了工厂实现能够动态创建不同的具体子类实例,必需有一个共同的基类或者共享工厂将创建其实例的所有的类实现的接口。现在发生的这种进退两难的局面就是,你如何能完成这种去藕情形,而不被强制为一切东西都创建一个接口。
这就是使用常规的工厂实现法实现DI所要面对的一些问题。然而,就如你很快能看到的,存在另一种可行的选择。同时,DI并不是单独围着工厂模式建立的,事实上,还跟许多其它模式相关,包括创建者,装配器,访问者模式。
使用容器抽象DI
许多先前的针对DI的缺点,通过使用容器都能够解决,容器是一个把一些抽象驻留在它的墙之内的隔离间。典型地,对象管理的责任由任何被用来管理这些对象的容器来接管,然而,容器也能接管实例化、配置,特定容器的程序也为对象服务。
容器考虑到对象被容器配置,因此反对通过客户端程序配置。这考虑到容器服务一个广泛职责功能范围,例如对象生命周期管理和依赖性分解。另外,容器能对对象应用交叉服务无论构造是否驻留在容器内,交叉(cross-cutting)服务被定义为一个足够通用在应用程序跨越不同上下文时的服务,同时提供一些特殊的好处。交叉(cross-cutting)服务一个例子就是日志框架,把程序中所有方法调用记入日志。
容器vs.工厂
有几个理由在你的程序开发中使用容器。容器提用许多其它服务提供包装普通对象的能力。这允许对象对某种基础架构和管线(plumbing)细节一无所知,如事务和基于角色的安全。时常地,客户端代码不需要清楚容器,因此没有真正的依赖性在容器自己之上。
这些服务能公开地配置,意味着它们能经由外部的方法来配置,包括GUIs,XML文件,属性文件,或者普通的.NET特性。
有横切(cross-cutting)服务的容器能跨程序被重用。一个容器能被用来跨越在企业的各种程序中配置对象。许多能被横跨整个企业来应用的服务是低层次的底层架构和管线(plumbing)服务。这些服务能跨整个企业被使用且不需要在一个程序中深度嵌入特定容器代码或者逻辑。
容器不是新事物
容器以一些形式或者其它存在很多年了,事实上,当Microsoft® Transaction Server (MTS)作为Windows NT® 4.0可选包被发布时,容器就被在后台使用了。
今天容器仍然是微软企业开发策略中活跃的一部分。事实上,假如你正写基于.NET的代码,你已经使用容器部署你的程序了:.NET公共语言运行时(CLR)。CLR在运行时执行广泛种类的重要任务,包括内存管理,自动范围检查和溢出保护,还有方法调用安全等。
新一版本的MTS,称之为COM+,是一个主要的进展。在.NET中的等同物,是企业服务,仍然被推荐为构造分布式企业应用的方法。COM+和企业服务提供超出MTS起初提供的大量的服务。在.NET1.1版本中,包括对象消息,对象池,便于陈述的动态事物,松耦合事件,基于角色的安全。
使用这些容器的问题就是它们太昂贵。尽管在它们之上能建立相当稳定架构,现在的容器技术对.NET开发员来说有一些缺陷。它们要求特定容器结构被引入域代码。很多操作能使容器基础架构逆向影响性能,即使是最低限度。
需要特定容器结构的例子可以在.NET Framework 1.x中找到,企业服务(Enterprise Service)要求任何在它控制下的必需派生自ServiceComponent类。既然.NET不支持多继承,这个约束限制了企业服务(Enterprise Service)能被使用的地方。
因为重量级容器影响性能和增加客户端代码的复杂性,所以仅在大型分布式应用中采取它们。
微软也提供了内置的对轻量级依赖注入(DI)版本的支持,使用的是System.ComponentModel名称空间。这不像企业服务,它不提供任何额外的服务或者功能;它只提供了服务注入。然而,像企业服务(Enterprise Service)为了使用在System.ComponentModel名称空间下的类,你的类必变成有容器意识的(container-aware)。这通过实现某特定的容器接口来实现。
轻量级容器
有许多程序将会从我描述的容器的许多特点中获得好处,但是他们的需要不证明使用重量级的容器是恰当的。在容器领域的另一端,轻量级容器提供了许多重量级容器所具备的相同的好处,而不需要COM+和企业服务的那些开支。尽管存在一些轻量级的容器,许多组织仍然选择使用Enterprise Service,但是这种情况正在改变,许多这些轻量级的容器除了提供简单的DI外还有其它服务。这些容器通常能被配置向你的对象中增加其它有价值的服务。
Spring.NET
你能建立你自己的轻量级DI容器,尽管已经存在了一些你认为可以利用的这类系统的实现。其中一个就是Spring.NET,Spring.NET提供了一个围绕工厂概念建立的轻量级的DI容器。它不仅通过允许用户使用在他们代码中预建的工厂来提供DI,也提供一组能应用在Spring.NET控制之下任何对象的服务。由于Spring.NET使用标准的.NET代码建立的,使用Spring.NET程序不需要额外依赖COM,COM+,或者Enterprise Service。
Factory Example工厂例子
下面代码是一个简单的接口,IDomainObjectInerface,我的对象将实现它。该接口包含一个属性,它返回一个表示我的对象名称的字符串
public interface IDomainObjectInterface
{
string Name{ get; }
}
在 图 2 中的代码(译者注:为了方便,我把这些代码都放进文章,你当然也可以点击链接)
包含两个类,都实现上述的接口。如你所见,那个Name属性简单地返回类的名称,这依赖于被使用的具体子类。我将使用这两个类,连同他们实现的接口,作为我即将介绍的例子的基础。
图2 实现类
public class ImplementationClass1 : IDomainObjectInterface {
public ImplementationClass1(){}
public string Name
{
get { return "Implementation Class 1"; }
}
}
public class ImplementationClass2 : IDomainObjectInterface {
public ImplementationClass2(){}
public string Name
{
get { return "Implementation Class 2"; }
}
}
图2
典型地,这两个类都将由一个工厂类创建。类似于图3.另外工厂类,ImplementationClassFactory,图3.也包含枚举,ImplementationClassType.工厂类有一个方法,GetImplementationClass,它接收一个枚举值,根据枚举的值,将返回这两个IDomainObjectInterface实现类的其中一个。客户端的类复杂选择它将使用哪个实现类。
图3 工厂类
public enum ImplementationClassType
{
ImplementationClass1,
ImplementationClass2
}
public class ImplementationClassFactory
{
public static IDomainObjectInterface GetImplementationClass(
ImplementationClassType implementationClassType )
{
switch ( implementationClassType )
{
case ImplementationClassType.ImplementationClass1:
return new ImplementationClass1();
case ImplementationClassType.ImplementationClass2:
return new ImplementationClass2();
default:
throw new ArgumentException("Class " +
implementationClassType + " not supported." );
}
}
}
现在,这个工厂方法有几个缺点。
首先,实现类的数目被写死在工厂方法中。因此,对于实现者即使有一个接口,但工厂方法也不可能返回一个它不了解的实现类。这限制了扩展性,特别是在公共API和程序框架这种情况下,我们不仅渴望知道,在什么地方引入新的实现类,而且达到某种程度的伸缩性。
其次,即使存在动态引入新的实现类的能力,客户端程序仍然需要知道请求哪一个类,这限制了工厂类被期望提供的一些伸缩性。
在图 4 中ConsoleRunner类说明了客户端如何使用工厂类创建一个想要的实现类的实例。注意,客户端代码必须明白的指定想要的实现类,在这点上,工厂类的许多好处就被丢失了。
图4 使用工厂类
using System;
using SpringDIExample;
class ConsoleRunner
{
static void
Main (){
IDomainObjectInterface domainObjectInterface =
ImplementationClassFactory.GetImplementationClass(
ImplementationClassType.ImplementationClass1);
Console.WriteLine("My name is " + domainObjectInterface.Name);
}
}
Spring.NET 实现
现在,你已经看过典型的工厂模式了,现在看一下,一个DI容器如何不仅达到许多与工厂方法相同目标,而且在你程序中增加大量的伸缩性和功能。
图 5了一个ConsoleRunner类的一个升级版本,在这个例子中,我们使用Spring.NET的DI容器,这需要一点初始化设置,首先,你必须建立一个工厂的实例,使用一个config.xml文件作为你的对象定义的源。接下来,用对Spring.NET工厂类的调用替换对自定义工厂类的的调用,注意,由于通用工厂不了解第三方接口的任何东西,所以任何从工厂返回的实例都向上转型为你所期望的接口。最后,ConsoleRunner类的最后一行保持不变,即使你已经了对象的源以及如何被实例化。
图5 ConsoleRunner 使用 Spring.NET
using System;
using System.IO;
using Spring.Objects.Factory.Xml;
using SpringDIExample;
class ConsoleRunner
{
static void
Main (){
// 1. Open the configuration file and create a new
// factory, reading in the object definitions
using (Stream stream = File.OpenRead("config.xml"))
{
// 2. Create a new object factory
XmlObjectFactory xmlObjectFactory =
new XmlObjectFactory(stream);
// 3. Call my factory class with generic label for the object
// that is requested.
IDomainObjectInterface domainObjectInterface =
(IDomainObjectInterface)xmlObjectFactory.
GetObject("DomainObjectImplementationClass");
// 4. Use the object just like any other concrete class.
Console.WriteLine("My name is " + domainObjectInterface.Name);
}
}
}
现在,让我们看下如何编写config.xml文件,就是它驱动工厂类,这儿是一个完整的config.xml文件(译者注:如果使用最新的Spring.net,可能稍有区别)中:
<?xml version="1.0" encoding="utf-8" ?>
<objects xmlns="http://www.springframework.net">
<object name="DomainObjectImplementationClass"
singleton="false"
type="ImplementationClass1, SpringDIExample" />
</objects>
你将注意到配置文件不是很大,仅包含两个元素,<object>,它包含所有的对象定义,和单独的<object>定义。从配置文件显示的,你能明白有一个对象定义,对象定义包含三个基本特性,这些特性定义了创建什么对象以及如何创建。
特性name定义了当从一个工厂请求一个对象时供我的ConsoleRunner类使用的名称。在这个例子中,是"DomainObjectImplementationClass".在客户端代码中,这个名称仅被使用来引用包含在配置文件定义。
接下来,singleton特性是一个Boolean标志,指定是否对象以singleton模式被创建。Spring.NET已经内建支持建立singleton对象,但如果我们不需要这功能,我就设该特性为false。
最后,type特性定义被创建对象的实际类型,当工厂被查询时,实际上,该类型将被加载和返回。该字符传接受"Type, Assembly"的形式。不仅指示对象的类型,而且指出它处于哪个程序集中。
通过简单地改变配置文件中实现类的类型,例如从"ImplementationClass1" t到"ImplementationClass2",你就能动态改变返回给客户端的类,不需要重新编译。
增强扩展性
直到现在,我已经简单地把创建对象的责任移到了一个外部的工厂实现和配置文件。虽然这种能被看到的公开配置的形式比那种静态的更让人满意,但自定义的工厂实现,有更多能使用容器来完成的东西。
假定你已经在公共的API中发布了IDomainObjectInterface接口,且你允许API的使用者创建他们自己的接口实现,all while still utilizing several existing clients that have already been built to use the IDomainObjectInterface.把使用者的实现给客户端证明是很困难的,特别是由于你根本不了解类如何被创建或者配置。类ImplementationClass3是一个IDomainObjectInterface接口的第三方实现,除了它被放置在一个单独的程序集中外,它类似于ImplementationClass1和ImplementationClass2,这两个类是在同一个程序集中的。
public class ImplementationClass3 : IDomainObjectInterface
{
public ImplementationClass3(){}
public string Name
{
get { return "Implementation Class 3"; }
}
}
使用如Spring.NET那样的框架,让我的ConsoleRunner类使用新的ImplementationClass3类是很容易的,仅需要的原始的config.xml配置,下面的配置文件已经做了必要的修改了。惟一不同的一行就是type特性,它已经更新为指向ImplementationClass3已经SimpleDIExampleExtension程序集:
<?xml version="1.0" encoding="utf-8" ?>
<objects xmlns="http://www.springframework.net">
<object name="DomainObjectImplementationClass"
singleton="false"
type="ImplementationClass3, SpringDIExampleExtension"/>
</objects>
重新运行ConsoleRunner,类ImplementationClass3的一个实例将被创建和返回。完成这些根本不需要重新编译ConsoleRunner,即使ImplementationClass3存在于一个与原始实现类物理分离的程序集。
依赖分解
现在你已经看到容器是如何帮助创建对象的,现在让我们看看对象间的依赖性是如何处理的。下面的类DependentClass,有一个读/写属性Message。
public class DependentClass
{
private string _message;
public DependentClass(){}
public string Message
{
set { _message = value; }
get { return _message; }
}
}
我们将配置容器自动把一个消息插入DependentClass.Message属性,我们也会动态把一个已配置DependentClass对象实例插入ImplementationClass4.DependentClass属性。
图 6显示了IDomainObjectInterface的一个新的实现,ImplementationClass4.如你所见,ImplementationClass4不仅实现了IDomainObjectInterface,它还有一个额外的属性,DependentClass,它将持有一个DependentClass的实例。
Figure 6 New Implementation of IDomainObjectInterface
public class ImplementationClass4 : IDomainObjectInterface
{
private DependentClass _dependentClass;
public ImplementationClass4(){}
public DependentClass DependentClass
{
get { return _dependentClass; }
set { _dependentClass = value; }
}
public string Name
{
get { return _dependentClass.Message; }
}
}
图7显示的是更新后的config.xml文件,与先前的相比,有三个改变,第一,增加了一个新的<object>元素,它配置被使用的DependentClass的一个实例。所有先前解释的<object>元素的特性都出现了,但是这个对象定义在主对象定义下面还有一个额外的元素。<property>元素为一个给定的对象定义配置一个属性。在这个例子中,特性name包含了要组装的属性的名称,在这儿就是DependentClass.Message.既然DependentClass.Message属性一个基本类型,它的设定值包装在一个<tag>标签中包含在该标签中的文本就是将被在实例中组装的DependentClass.Message的设定的值。
Figure 7 Updated config.xml
<?xml version="1.0" encoding="utf-8" ?>
<objects xmlns="http://www.springframework.net">
<object name="DomainObjectImplementationClass"
singleton="false"
type="ImplementationClass4, SpringDIExample">
<property name="DependentClass">
<ref object="DomainObjectDependentClass"/>
</property>
</object>
<object name="DomainObjectDependentClass"
singleton="false"
type="DependentClass, SpringDIExample">
<property name="Message"><value>Dependent Class</value></property>
</object>
</objects>
第二个改变就是涉及到我原始的DomainObjectImplementationClass定义。一个新的<property>元素被加入定义中,DependentClass属性,由于该属性的值是一个复杂类型的实例,一个<ref>标签被用来代替<value>标签来包装值。<ref>标签的object特性引用的是先前配置的对象定义的名称,在例中是"DomainObjectDependentClass".
最后对配置文件的改变,是在第一个对象定义中,我们已经用类ImplementationClass4更新了type的引用。
现在,重新部署新的config.xml文件,重新运行ConsoleRunner类,注意那被配置的DependentClass.Message属性被显示出来,依赖性被装配和分解,客户端程序正使用新的类,所有这项不需要知道正在使用什么类,也不需要重新编译。
结论
依赖注入是一个很值得探索的概念,为了在你开发的程序中使用它。它不但减少组件之间的耦合,也节省你一遍又一遍的写编写模板工厂的创建代码。Spring.NET是一个提供DI容器的框架的例子,但它不是惟一的.NET轻量级的容器,其它容器包括Pico 和 Avalon.