依赖注入

部署运行你感兴趣的模型镜像

今天比以往更加注重对现有组件的重用和把异构组件联结成一种粘合框架。但是这种联结很快就成了一项让人畏缩的任务,因为这个时候程序的尺寸和复杂度都在增加,依赖性也是。减少这种依赖性扩展的一个方法就是使用依赖注入(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)。这通过实现某特定的容器接口来实现。

Spring.NETSpring.NET

轻量级容器

有许多程序将会从我描述的容器的许多特点中获得好处,但是他们的需要不证明使用重量级的容器是恰当的。在容器领域的另一端,轻量级容器提供了许多重量级容器所具备的相同的好处,而不需要COM+和企业服务的那些开支。尽管存在一些轻量级的容器,许多组织仍然选择使用Enterprise Service,但是这种情况正在改变,许多这些轻量级的容器除了提供简单的DI外还有其它服务。这些容器通常能被配置向你的对象中增加其它有价值的服务。



你能建立你自己的轻量级DI容器,尽管已经存在了一些你认为可以利用的这类系统的实现。其中一个就是,Spring.NET提供了一个围绕工厂概念建立的轻量级的DI容器。它不仅通过允许用户使用在他们代码中预建的工厂来提供DI,也提供一组能应用在Spring.NET控制之下任何对象的服务。由于Spring.NET使用标准的.NET代码建立的,使用Spring.NET程序不需要额外依赖COM,COM+,或者Enterprise Service


Factory Example工厂例子 2

下面代码是一个简单的接口,IDomainObjectInerface,我的对象将实现它。该接口包含一个属性,它返回一个表示我的对象名称的字符串

public interface IDomainObjectInterface

{

string Name{ get; }

}

中的代码(译者注:为了方便,我把这些代码都放进文章,你当然也可以点击链接

包含两个类,都实现上述的接口。如你所见,那个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.另外工厂类,ImplementationClassFactory3.也包含枚举,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 实现 5

现在,你已经看过典型的工厂模式了,现在看一下,一个DI容器如何不仅达到许多与工厂方法相同目标,而且在你程序中增加大量的伸缩性和功能。

了一个ConsoleRunner类的一个升级版本,在这个例子中,我们使用Spring.NETDI容器,这需要一点初始化设置,首先,你必须建立一个工厂的实例,使用一个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",你就能动态改变返回给客户端的类,不需要重新编译。


增强扩展性依赖分解 6

直到现在,我已经简单地把创建对象的责任移到了一个外部的工厂实现和配置文件。虽然这种能被看到的公开配置的形式比那种静态的更让人满意,但自定义的工厂实现,有更多能使用容器来完成的东西。

假定你已经在公共的API中发布了IDomainObjectInterface接口,且你允许API的使用者创建他们自己的接口实现,all while still utilizing several existing clients that have already been built to use the IDomainObjectInterface.把使用者的实现给客户端证明是很困难的,特别是由于你根本不了解类如何被创建或者配置。类ImplementationClass3是一个IDomainObjectInterface接口的第三方实现,除了它被放置在一个单独的程序集中外,它类似于ImplementationClass1ImplementationClass2,这两个类是在同一个程序集中的。

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属性。

显示了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.

 

 

 

Contents

Factory Patterns Refresher
DI Implementation Using Factories
Abstracting DI Using Containers
Containers vs. Factories
Containers Are Not New
Lightweight Containers
Spring.NET
Factory Example
A Spring.NET Implementation
Enhancing Extensibility
Dependency Resolution
Conclusion


 

Today there is a greater focus than ever on reusing existing components and wiring together disparate components to form a cohesive architecture. But this wiring can quickly become a daunting task because as application size and complexity increase, so do dependencies. One way to mitigate the proliferation of dependencies is by using Dependency Injection (DI), which allows you to inject objects into a class, rather than relying on the class to create the object itself.

The use of a factory class is one common way to implement DI. When a component creates a private instance of another class, it internalizes the initialization logic within the component. This initialization logic is rarely reusable outside of the creating component, and therefore must be duplicated for any other class that requires an instance of the created class. For example, if class Foo creates an instance of class Bar and instances of Bar require several initialization steps, different for each instance of Bar, other classes that create instances of Bar will have to reproduce the same initialization logic found within Foo.

Developers like to automate monotonous and menial tasks, and yet most developers still perform functions such as object construction and dependency resolution by hand. Dependency resolution can be described as the resolving of defined dependencies of a type or object. Dependency Injection, on the other hand, aims to reduce the amount of boilerplate wiring and infrastructure code that you must write.

Containers provide a layer of abstraction in which to house components. DI containers, in particular, reduce the kind of dependency coupling I just described by providing generic factory classes that instantiate instances of classes. These instances are then configured by the container, allowing construction logic to be reused on a broader level.

Before diving into DI containers, let's first review a core pattern used through DI containers, the Abstract Factory pattern.


Factory Patterns Refresher

In Design Patterns (Addison-Wesley, 1995), authors Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides describe the intent of the Abstract Factory pattern like this: "To construct and instantiate a set of related objects without specifying their concrete objects." Utilizing the Abstract Factory pattern in your applications allows you to define abstract classes to create families of objects. By encapsulating the instantiation and construction logic, you retain control over the dependencies and the allowable states of your objects.

Frequently, objects need to be instantiated in a coordinated fashion, usually because of certain dependencies or other requirements. For example, when creating an instance of System.Xml.XmlValidatingReader in client code, an XmlSchemaCollection object is frequently populated with the relevant schemas for use when validating the XmlValidatingReader object. This is an example of needing to not only create an instance of a class, but also to configure it after creation and before it can be used.

Another type of factory pattern is called the factory method. A factory method is simply a method, usually defined as static, whose sole purpose is to return an instance of a class. Sometimes, in the case of facilitating polymorphism, a flag will be passed to the factory method to indicate the specific interface implementation or subclass to be returned. For example, the Create method of WebRequest takes in either a string or Uri instance, and returns a new instance of a class derived from WebRequest.

From this point forward, I will simply use the word "factories" to mean both the Abstract Factory pattern as well as the factory method implementation.

Back to top


DI Implementation Using Factories

Factories allow for an application to wire together objects and components without exposing too much information about how the components fit together or what dependencies each component might have. Instead of spreading complex creation code around an application, factories allow for that code to be housed in a central location, thereby facilitating reuse throughout the application. Client code then calls creation methods on the factory, with the factory returning complete instances of the requested classes. 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
Figure 1 Factory Functions

Factories can do more than simply create objects and assemble their dependencies. They can also serve as a central configuration area for applying services or constraints uniformly across all instances of an object (see Figure 1). For example, instead of returning an instance of an object, a factory can return a proxy to the real instance of the object, thereby enabling distributed method calls. Since the client application is unaware that the object it is being handed is, in fact, a proxy, as opposed to the real instance of the object, no changes to the client code need to occur. An example of this type of service can be found within the .NET remoting infrastructure. Distributed objects can be configured declaratively, with a .NET configuration file, and the client application can simply create an instance of the class using "new". This is the same for local and distributed objects, as well as for client-activated objects and server-activated objects. All of this configuration and management takes place without the client application knowing about .NET remoting.

However, factories are not without drawbacks. While a factory implementation can be quite valuable for a certain application, most of the time, it is not reusable across other applications. Frequently all of the available creation options are hardcoded into the factory implementation, making the factory itself non-extensible. Also, most of the time the class calling the factories' creation methods must know which subclass of the factory to create.

Secondly, all dependencies for an object that is created using a factory are known to the factory at compile time. Leaving .NET reflection out of the picture for a moment, at run time there is no way to insert or alter the manner in which objects are created, or which dependencies are populated. This all must happen at design time, or at least require a recompile. For example, if a factory is creating instances of class Foo and instances of class Foo require an instance of class Bar, then the factory must know how to retrieve an instance of the Bar class. The factory could create a new instance, or even make a call to another factory.

Third, since factories are custom to each individual implementation, there can be a significant level of cross-cutting infrastructure that is held captive inside a particular factory. Take my example of a factory dynamically substituting a proxy object for a real object. That is an example of a piece of infrastructure, namely the wrapping of simple objects for deployment over a distributed wire, that is completely encapsulated inside that particular factory. If another object needs to be altered in a similar manner, the logic to do so is hidden inside a factory, and would have to be repeated for the other object. Once this functionality is desired outside of the original application, the problem now becomes how to reuse such functionality while still maintaining the existing factory concept.

Lastly, factories rely on well-defined interfaces to achieve polymorphism. In order for a factory implementation to be able to dynamically create different concrete subclass instances, there must be a common base class or shared interface implemented by all classes that the factory will create. Interfaces decouple the construction of the object from the specific implementation of the interface. The dilemma that arises now is how you can accomplish this decoupling without being forced to create an interface for everything.

These are just some of the problems facing DI implemented using conventional factory implementations. However, as you will see shortly, another viable option exists. Also, DI is not based solely around the factory pattern and in fact has many correlations with many other patterns, including the Builder, Assembly, and Visitor patterns. For more information on these useful patterns, the Design Patterns book (already mentioned) is the seminal reference.

Back to top


Abstracting DI Using Containers

Many of the previous shortcomings to DI can be solved by using a container. A container is a compartment that houses some sort of abstraction within its walls. Typically, responsibility for object management is taken over by whatever container is being used to manage those objects. However, containers can also take over instantiations, configuration, as well as the application of container-specific services to objects.

Containers allow for objects to be configured by the container, as opposed to being configured by the client application. This allows for the container to serve a wide range of functions, such as object lifecycle management and dependency resolution. In addition, containers can apply cross-cutting services to whatever construct is being hosted inside the container. A cross-cutting service is defined as a service that is generic enough to be applicable across different contexts, while providing specific benefits. An example of a cross-cutting service is a logging framework that logs all method calls within an application.

Back to top


Containers vs. Factories

There are several reasons to use containers in your application development. Containers provide the ability to wrap vanilla objects with a wealth of other services. This allows the objects to remain ignorant about certain infrastructure and plumbing details, like transactionality and role-based security. Oftentimes, the client code does not need to be aware of the container, so there is no real dependency on the container itself.

These services can be configured declaratively, meaning they can be configured via some external means, including GUIs, XML files, property files, or vanilla .NET-based attributes.

Containers that have cross-cutting services are also reusable across applications. One container can be used to configure objects across various applications within an enterprise. Many services that can be applied across an enterprise are low-level infrastructure and plumbing services. These services can be used across an enterprise without the need to deeply embed container-specific code or logic within an application.

Back to top


Containers Are Not New

Containers have been around in some form or another for many years. As a matter of fact, containers were used back when Microsoft® Transaction Server (MTS) was released as part of the Windows NT® 4.0 option pack.

Containers are still an active part of Microsoft enterprise development strategy today. In fact, if you're writing .NET-based code, you're already using a container to deploy your application: the .NET common language runtime (CLR). The CLR performs a wide variety of important tasks at run time, including memory management, automatic bounds checking and overflow protection, and method call security, to name a few.

The next version of MTS, dubbed COM+, was a major evolution. The .NET equivalent, Enterprise Services, is still the recommended approach to constructing distributed enterprise applications. COM+ and Enterprise Services offer a wealth of services beyond what Microsoft Transaction Server originally offered. In the current version, this includes object messaging, object pooling, declarative automatic transactions, loosely coupled events, and role-based security.

The problem with some containers is that they can be costly. Despite being built upon a fairly stable architecture, the current container technology available to developers using .NET has some drawbacks. They require container-specific constructs to be introduced into domain code. Container infrastructure can adversely impact performance for many operations, even if only minimally.

An example of requiring container-specific constructs can be found in the Microsoft .NET Framework 1.x Enterprise Services requirement that any object that is under its control must derive from the ServicedComponent class. Since .NET does not support multiple inheritance, this constraint limits where Enterprise Services can be utilized.

Since heavyweight containers impact performance and increase the complexity of the client application, they are usually employed in only the largest distributed applications.

Microsoft also offers built-in support for a lightweight version of Dependency Injection, with the System.ComponentModel namespace. Unlike EnterpriseServices, it does not provide any extra services or functionality; it merely provides service injection. However, like Enterprise Services, in order to use the classes within the System.ComponentModel namespace, your classes must become container-aware. This is accomplished by implementing certain container-specific interfaces.

Back to top


Lightweight Containers

There are a great number of applications that would benefit from many of the features of the containers I described, but their needs don't justify the use of a heavyweight container. At the opposite end of the containers spectrum, lightweight containers provide many of the same benefits that the heavyweight containers do, but without all of the overhead of COM+ and Enterprise Services. Many organizations still choose to use Enterprise Services in spite of the existence of lightweight containers, but this situation is changing. Many of these lightweight containers offer services in addition to simple DI. The containers can often be configured to add other valuable services to your objects.

Back to top


Spring.NET

You can build your own lightweight DI container, though a few implementations of such systems already exist which you might consider taking advantage of. One such solution is Spring.NET, which I'll be using for the rest of this column to demonstrate some of the ideas discussed thus far. Spring.NET offers a lightweight DI container built around the concept of factories. It not only provides DI by allowing users to use pre-built factories within their code, but it also provides a suite of services that can be applied to any object under Spring.NET's control. And since Spring.NET is built using standard .NET-based code, applications that utilize Spring.NET have no additional dependency on COM, COM+, or Enterprise Services.

Back to top


Factory Example

The following code shippet is a simple interface, IDomainObjectInterface, which my objects will implement. The interface contains one property, which returns a string representation of the name of my object:

public interface IDomainObjectInterface
  
{
  
    string Name{ get; }
  
}
  

The code in Figure 2 contains two classes that implement the interface. As you can see, the Name property simply returns the name of the class, depending on which concrete class is used. I will use these two classes, as well as the interface they implement, as the basis of the example I will present.

Typically, either of these two classes would be created by a factory class, similar to the one in Figure 3. In addition to the factory class, ImplementationClassFactory, Figure 3 also contains one enumeration, ImplementationClassType. The factory class has one method, GetImplementationClass, which accepts one of the enumeration values. Based on the value of the enumeration, one of the two IDomainObjectInterface implementations will be returned. The client class is responsible for choosing which implementation it would like to use.

Now, there are several drawbacks to this factory method. The first is that the number of implementation classes is hardwired into the factory method. Therefore, even though there is an interface for the implementation objects, it's impossible for the factory method to return an implementation class that it does not know about. This limits extensibility, especially in the case of a public API and application frameworks, where the ability to dynamically introduce new implementation classes is not only desired, but often expected to achieve a certain degree of flexibility.

Secondly, even if the ability to dynamically introduce new implementations existed, the client application would still need to know which class to ask for. This eliminates some of the flexibility that the factory class was supposed to provide.

The ConsoleRunner class in Figure 4 illustrates how a client would use the factory class to create an instance of the desired implementation class. Notice how the client code has to explicitly ask for the desired implementation class. At this point many of the benefits of the factory class have been negated.

Back to top


A Spring.NET Implementation

Now that you have seen the typical factory pattern, let's take a look at how a DI container not only achieves many of the same goals, but also adds a significant amount of flexibility and functionality to your application.

Figure 5 contains an updated version of the ConsoleRunner class. For this example I'm using the Spring.NET DI container, which requires a bit of initial setup. First, you must create an instance of the factory, using a config.xml file as the source of your object definitions. Next, replace a call to the custom factory class with a call to the Spring.NET factory class. Notice that since the generic factory doesn't know anything about third-party interfaces, everything coming back from the factory is downcast to object. So you must upcast the object instances returned by the factory to the interface that you expect. Finally, the last line of the ConsoleRunner class remains unaffected, even though you have changed the source of the object and how it's instantiated.

Now, let's take a look at the make up of the config.xml file, which drives the factory class. Here is the full config.xml file:

<?xml version="1.0" encoding="utf-8" ?>
  
<objects xmlns="http://www.springframework.net">
  
    <object name="DomainObjectImplementationClass" 
  
            singleton="false" 
  
            type="ImplementationClass1, SpringDIExample" />
  
</objects>
  

You will notice that the configuration file is not very large, being comprised of only two elements, <objects>, which contain all of the object definitions, and the individual <object> definitions. From the configuration file shown, you can see that there is one object definition. The object definition contains three basic attributes that define what object is created as well as how it's created.

The name attribute defines the name that my ConsoleRunner class uses when requesting an object from the factory. In this case, it is "DomainObjectImplementationClass". This name is just used to reference the definitions contained in the configuration file from the client code.

Next, the singleton attribute is a Boolean flag that designates if the object should be created as a singleton or not. Spring.NET has built-in support for making objects singletons, but since I do not require such functionality, I set this attribute to false.

Finally, the type attribute defines the actual type of the object to be created. This is the type that will actually be loaded and returned when the factory is queried. This string takes the form of "Type, Assembly", indicating not only the type of object, but also which assembly the type is located in. As indicated in the configuration file, the type desired is one of the types shown in Figure 2.

By simply changing the type of the implementation class listed in the configuration file from "ImplementationClass1" to "ImplementationClass2", you can dynamically alter the class that is returned to the client, all without a recompile.

Back to top


Enhancing Extensibility

Until now, I have simply moved responsibility of object creation to an external factory implementation and configuration file. While this type of declarative configuration can be seen as more desirable than a static, custom factory implementation, there is much more that can be accomplished by using containers.

Let's say that you have published the IDomainObjectInterface in a public API, and you would like to allow users of the API to create their own implementations of the interface, all while still utilizing several existing clients that have already been built to use the IDomainObjectInterface. Getting the users' implementation to your clients could prove to be difficult, especially since you know nothing about how the class will be built or deployed. ImplementationClass3 is a third implementation of the IDomainObjectInterface, which is similar to the ImplementationClass1 and ImplementationClass2 classes, except this one resides in a separate assembly. Previously, both implementation classes and the interface resided in the same assembly:

public class ImplementationClass3 : IDomainObjectInterface
  
{
  
    public ImplementationClass3(){}
  
    public string Name
  
    {
  
        get { return "Implementation Class 3"; }
  
    }
  
}
  

Using a framework like Spring.NET, getting my ConsoleRunner class to use the new ImplementationClass3 is easy to accomplish, and only requires a minor change to my original config.xml configuration file. The following is the configuration file with the necessary changes made. The only difference lies within the type attribute, which has been updated to point to the ImplemenationClass3 type and the SimpleDIExampleExtension assembly:

<?xml version="1.0" encoding="utf-8" ?>
  
<objects xmlns="http://www.springframework.net">
  
    <object name="DomainObjectImplementationClass" 
  
            singleton="false" 
  
            type="ImplementationClass3, SpringDIExampleExtension"/>
  
</objects>
  

When the ConsoleRunner class is rerun, an instance of ImplementationClass3 will be instantiated and returned. All of this is accomplished without a recompile of ConsoleRunner, even though ImplemenationClass3 resides in an assembly that's physically separate from the original implementation classes.

Back to top


Dependency Resolution

Now that you have seen how containers can aid in object creation, let's take a look at how dependencies between objects are handled. The following class, DependentClass, has one read/write property that contains a message for the class to hold:

public class DependentClass
  
{
  
    private string _message;
  

  
    
  
    public DependentClass(){}
  

  
    
  
    public string Message
  
    {
  
        set { _message = value; }
  
        get { return _message; }
  
    }
  
}
  

I will configure the container to automatically insert a message into the DependentClass.Message property. I will also dynamically insert an instance of the configured DependentClass object into the ImplemenationClass4.DependentClass property.

Figure 6 shows a new implementation of IDomainObjectInterface, ImplemenationClass4. As you can see, not only does ImplementationClass4 implement the IDomainObjectInterface, it also has an extra property, DependentClass, which holds an instance of DependentClass.

Figure 7 shows my updated config.xml file. There are three changes from the configuration files shown previously. The first is the addition of a new <object> element that configures an instance of the DependentClass to be used. All of the previously explained attributes of the <object> element are present, but this object definition has an extra element underneath the main object definition. The <property> element configures a property for a given object definition. In this case the name attribute contains the name of the property to populate; here it's DependentClass.Message. Since the DependentClass.Message property is a basic type, its configured value is wrapped in <value> tags. The text contained inside these tags is the configured value of the DependentClass.Message that will be populated at instantiation.

The second change concerns my original DomainObjectImplementationClass definition. A new <property> element has been added to the definition, configured to populate the ImplementationClass4.DependentClass property. Since the value of this property is an instance of a complex type, a <ref> tag is used instead of wrapping the value in <value> tags. The object attribute of the <ref> tags references the name of a previously configured object definition, in this case, "DomainObjectDependentClass".

The last change to the configuration file can be found within the first object definition. I have updated the type reference to use the ImplementationClass4 class.

Now, redeploy the new config.xml file, and rerun the ConsoleRunner class. Notice that the configured DependentClass.Message property is displayed. The dependencies have been populated and resolved and the client app is using the new classes, all without knowing what class it's using and all without requiring a recompile.

Back to top


Conclusion

Dependency Injection is a worthwhile concept to explore for use within apps that you develop. Not only can it reduce coupling between components, but it also saves you from writing boilerplate factory creation code over and over again. Spring.NET is an example of a framework that provides a ready to use DI container, but it is not the only .NET lightweight container out there. Other containers include Pico and Avalon

 

 

您可能感兴趣的与本文相关的镜像

Dify

Dify

AI应用
Agent编排

Dify 是一款开源的大语言模型(LLM)应用开发平台,它结合了 后端即服务(Backend as a Service) 和LLMOps 的理念,让开发者能快速、高效地构建和部署生产级的生成式AI应用。 它提供了包含模型兼容支持、Prompt 编排界面、RAG 引擎、Agent 框架、工作流编排等核心技术栈,并且提供了易用的界面和API,让技术和非技术人员都能参与到AI应用的开发过程中

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值