DI/IOCs






4.83/5 (30投票s)
对依赖注入和IOC容器的探讨。
引言
在我工作的公司,我们开始使用各种敏捷/SCRUM/Xp方法论开发一个新项目,并且我们很幸运能请到马丁·福勒的公司:ThoughtWorks,来教我们一些东西。其中一个就是依赖注入。现在,CodeProject上已经有很多关于DI的文章了,但在阅读了一些之后,我觉得还有空间写一篇,因为我想谈谈它试图解决的问题,这是其他一些文章似乎没有涵盖到的。
IOCs解决了什么
通常,当我们编写软件时,我们编写的是调用服务类(不一定是Web服务)的软件;我的意思是提供某些功能的类。考虑以下代码,我期望能够使用某种日志服务和某种异常报告服务
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1
{
public class SomeClass
{
private ILoggingStrategy logger = null;
private IExceptionReporter exreporter = null;
public SomeClass()
{
//create a SQL logger
logger = new SQLLogger();
}
public void SomeMethod()
{
//create a SQL Exception reporter
exreporter = new SQLExceptionReporter();
}
}
}
大多数情况下,如果一个对象需要访问某个服务,实际的对象会承担创建该服务实例的角色(如上所示),代码需要。这可以采取多种形式:可以在构造函数中完成,或通过持有对服务对象的直接引用,或者甚至可以使用某种服务定位器来提供特定服务对象的实例。所有这些方法都在消耗对象内持有服务对象的引用,这会使消耗对象与服务对象实例紧密耦合,这可能不是理想的,因为消费者现在依赖于服务对象。
依赖注入的作用是允许将对服务类的这种依赖反转,这样它们就不再在消耗对象中创建了。
DI可以使用属性或构造函数参数,这些参数必须是所需的服务类型,并且当创建需要这些服务类型的对象时,所需服务类型的实现将由某个外部机制自动传递给正在构造的对象。 Bingo,消耗对象内不再存在对服务类型的内部依赖。
依赖注入比持有内部引用更灵活,因为它允许创建一个给定服务的替代实现,并将其注入到需要服务类型的对象中。实际的服务实现类型可以在配置文件中配置,因此可以轻松地将一个服务类型实现替换为另一个。设想一个场景,您编写了一些需要与SQL Server配合使用的软件,然后您的公司决定您还必须支持Oracle;如果我们使用DI和配置文件,我们可以将解决方案配置为持有Oracle服务类型或SQL Server服务类型,并且使用这些服务的代码可以在不重新编译的情况下工作。
DI特别有用的另一个领域是使用Mocks时;您可以通过简单地将配置文件设置为指向某个服务的Mock实现来使用Mock服务类型。这非常有用。我们已经在工作中这样做了,它为我们的单元/集成测试提供了很大的灵活性。
显然,这种程度的可配置性带来了所谓的接口爆炸,所以DI也有其缺点。然而,DI带来的好处很容易超过您最终需要处理的接口数量。显然,应该有一个有效且正当的理由使用DI;但值得思考一下。
市面上有一些不错的DI实现,允许通过配置文件/特殊属性来配置类型。通常,这些框架都以IOC(“控制反转”)的伞形名称提供DI。
根据维基百科,IOC是
“控制反转,或IoC,是一种抽象原则,它描述了某些软件架构设计的方面,其中系统的控制流与传统软件库的架构相比被反转。”
几乎所有(如果不是全部)IOC框架本质上都是服务类型的容器。您可以将容器想象成一个特殊的/聪明的字典,它知道如何获取服务类型并在需要时提供它。基本思想是,您有一个容器,它知道所有服务类型(或您希望与DI一起使用的其他类型)。当一个需要这些已知/存储在容器中的服务类型的对象需要一个时,IOC框架将注入这些服务类型。
在本篇文章的其余部分,我将讨论如何使用一些免费框架来实现DI,并在文章末尾提供更多DI/IOC框架的链接,您可以自行研究。
通用演示代码示例
附带的演示代码包含三个项目
- CastleWindsorIOC:展示了如何使用构造函数DI和泛型支持
- UnityIOC:展示了如何使用构造函数DI和泛型支持
- UnityIOCPropertyResolution:展示了如何使用属性DI
CastleWindsorIOC和UnityIOC项目都旨在演示完全相同的问题。它们旨在演示的问题非常简单。有一个名为IMessageFormatter
的单一接口,它有两个实现:DateTimeMessageFormatter
和SimpleMessageFormatter
,可以通过在配置文件中提供正确的设置来使用。
还有一个如何使用IGenericFormatter<T>
泛型接口的示例,其实现位于一个名为DummyObjectFormatter
的类中。
还有一个简单的Form
(Form1
)对象,它需要一个IMessageFormatter
和一个IGenericFormatter<DummyObject>
实现才能正确工作,这些实现将通过构造函数传递给它。
如果我向您展示类图,可能会更清楚。
Castle Windsor 容器
Castle Windsor容器是Castle项目的一部分,可以在http://www.castleproject.org/下载。
使用Castle Windsor容器的基本思想是,您有一个可以通过代码/配置文件配置的容器。
Castle Windsor 容器:配置
我喜欢使用配置文件,因为它提供了最大的灵活性,因为我们可以直接修改配置文件而无需重新编译代码。
这是一个配置文件示例
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!-- Custom Config Sections -->
<configSections>
<section
name="castle"
type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor" />
</configSections>
<!-- Castle Config Section -->
<castle>
<components>
<!-- Alternative Message Provider
<component
id="CurrentFormatter"
service="CastleWindsorIOC.IMessageFormatter, CastleWindsorIOC"
type="CastleWindsorIOC.SimpleMessageFormatter, CastleWindsorIOC" />
-->
<component
id="CurrentFormatter"
service="CastleWindsorIOC.IMessageFormatter, CastleWindsorIOC"
type="CastleWindsorIOC.DateTimeMessageFormatter, CastleWindsorIOC" />
<component id="DummyObjectFormatter"
service="CastleWindsorIOC.IGenericFormatter`1[[CastleWindsorIOC.DummyObject,
CastleWindsorIOC]], CastleWindsorIOC"
type="CastleWindsorIOC.DummyObjectFormatter, CastleWindsorIOC"/>
</components>
</castle>
</configuration>
可以看到,我们创建了一个自定义的Castle配置节,并在该自定义Castle节中,我们配置容器,告诉它应该能够解析哪些类型。这是通过为每个所需的类型添加一个component
元素来实现的。
我们可以看到这个配置文件中有两种类型(一种被注释掉了,您可以取消注释来尝试,但如果您想尝试当前注释掉的一种,请不要忘记注释掉当前的一种),它们是同一IMessageFormatter
接口的两种不同实现,因此有效地提供了两种不同的服务,尽管在这种情况下是非常简单的服务。
Castle Windsor 容器:构造函数支持
正如您从上面的类图看到的,有一个IMessageFormatter
接口,它有两种不同的实现(SimpleMessageFormatter
和DateTimeMessageFormatter
)。您刚才看到的配置文件显示了如何将这些类型添加到Castle Windsor容器中。
那么,我们如何使用Castle Windsor容器中的一种类型呢?嗯,这很简单。我们只需要这样做:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using Castle.Windsor;
using Castle.Windsor.Configuration.Interpreters;
using Castle.Core.Resource;
namespace CastleWindsorIOC
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
IWindsorContainer container =
new WindsorContainer(
new XmlInterpreter(new ConfigResource("castle")));
// use the Current Message Formatter (see the App.Config)
var currentIMessageFormatter =
(IMessageFormatter)container.Resolve("CurrentFormatter");
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1(currentIMessageFormatter));
}
}
}
从这个例子中可以看出,我们首先配置了Castle Windsor容器,然后从容器中获取所有需要的类型,然后构造一个新的Form1
对象,并将这些容器对象传递给Form1
对象的构造函数。
让我们来看看Form1
的代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace CastleWindsorIOC
{
public partial class Form1 : Form
{
private IMessageFormatter msgFormatter;
private int currentMessageCount = 0;
public Form1()
{
InitializeComponent();
}
public Form1(IMessageFormatter msgFormatter)
: this()
{
this.msgFormatter = msgFormatter;
}
private void btnGo_Click(object sender, EventArgs e)
{
lstItems.Items.Add(msgFormatter.FormatMessage(
String.Format("CurrentMessage {0}",
currentMessageCount++)));
}
.....
.....
}
}
从这里,我们可以看到Form1
对象在其构造函数中需要的类型已经被Castle Windsor容器解析并提供给了Form1
构造函数。很酷。
那么,当我们运行应用程序时会发生什么?我们得到的格式化消息是使用当前(App.Config类型)的IMessageFormatter
接口实现的。
Castle Windsor 容器:泛型支持
这样就不错了,我们现在可以通过Castle Windsor容器使用构造函数参数来实现DI了。那么我们还能做什么呢?肯定还有更多。是的,确实有。我们可以将DI与泛型结合使用。下面是一个例子。
演示代码中有一个IGenericFormatter<T>
接口,其声明如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CastleWindsorIOC
{
public interface IGenericFormatter<T>
{
String FormatMessage(T valueIn);
}
}
演示代码还包含该接口的一个实现,如下所示:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CastleWindsorIOC
{
public class DummyObjectFormatter : IGenericFormatter<DummyObject>
{
public string FormatMessage(DummyObject valueIn)
{
return String.Format("The value was {0}", valueIn.ToString());
}
}
}
这是您需要在App.Config文件中支持泛型的内容:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!-- Custom Config Sections -->
<configSections>
<section
name="castle"
type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor" />
</configSections>
<!-- Castle Config Section -->
<castle>
<components>
<!-- Alternative Message Provider
<component
id="CurrentFormatter"
service="CastleWindsorIOC.IMessageFormatter, CastleWindsorIOC"
type="CastleWindsorIOC.SimpleMessageFormatter, CastleWindsorIOC" />
-->
<component
id="CurrentFormatter"
service="CastleWindsorIOC.IMessageFormatter, CastleWindsorIOC"
type="CastleWindsorIOC.DateTimeMessageFormatter, CastleWindsorIOC" />
<component id="DummyObjectFormatter"
service="CastleWindsorIOC.IGenericFormatter`1[[CastleWindsorIOC.DummyObject,
CastleWindsorIOC]], CastleWindsorIOC"
type="CastleWindsorIOC.DummyObjectFormatter, CastleWindsorIOC"/>
</components>
</castle>
</configuration>
在这种情况下,我使用Unity.DummyObject
类型作为IGenericFormatter<T>
实现使用的类型,如果您还记得,它定义如下:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace CastleWindsorIOC
{
public class DummyObjectFormatter :
IGenericFormatter<DummyObject>
{
public string FormatMessage(DummyObject valueIn)
{
return String.Format("The value was {0}",
valueIn.ToString());
}
}
}
我们可以看到DummyObjectFormatter
期望从Castle Windsor容器中传递一个DummyObject
。我刚才展示的配置文件节正是这样做的。
我们如何使用Castle Windsor容器来解析这一点?嗯,我们只需要这样做:
// use the generic Message Formatter
var dummyObjectFormatter =
(IGenericFormatter<dummyobject>)container.Resolve("DummyObjectFormatter");
如果我们检查需要提供此类类型的Form1
对象的构造函数,应该很明显,我们所需要做的就是将容器获取的泛型对象传递给Form1
对象的构造函数。
public Form1(IMessageFormatter msgFormatter,
IGenericFormatter<dummyobject> actualGenTest)
: this()
{
this.msgFormatter = msgFormatter;
this.actualGenTest = actualGenTest;
}
Castle Windsor 容器:属性支持
不仅可以注入构造函数参数,还可以使用DI/IOC注入属性值。我尝试用Castle Windsor容器来实现这一点,但没有成功。我确定这可以做到,只是我没有成功……但是正如我所说,我认为这是可能的。
Unity Container
Unity是Microsoft的一个应用程序块,来自Patterns & Practices组,可以在http://msdn.microsoft.com/en-us/library/dd203101.aspx下载。
使用Unity的基本思想是,您有一个可以通过代码/配置文件配置的容器,这与Castle Windsor非常相似。我认为最好的演示方法是像我们之前对Castle Windsor那样,通过同一个小例子来演示。
那么,让我们开始吧。
Unity 容器:配置
我喜欢使用配置文件,因为它提供了最大的灵活性,因为我们可以直接修改配置文件而无需重新编译代码。
这是一个配置文件示例。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!-- Custom Config Sections -->
<configSections>
<section name="unity"
type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
Microsoft.Practices.Unity.Configuration" />
</configSections>
<!-- Unity Config Section -->
<unity>
<containers>
<container>
<types>
<!-- Alternative Message Provider
<type name="CurrentFormatter"
type="Unity.IMessageFormatter,Unity"
mapTo="Unity.SimpleMessageFormatter,Unity"/>
-->
<type name="CurrentFormatter"
type="Unity.IMessageFormatter,Unity"
mapTo="Unity.DateTimeMessageFormatter,Unity"/>
</types>
</container>
</containers>
</unity>
</configuration>
可以看到,我们创建了一个自定义的Unity配置节(与Castle Windsor非常相似),并在该自定义Unity节中,我们配置容器,告诉容器在运行时它将知道哪些类型。
我们可以看到这个配置文件中有两种类型(一种被注释掉了,您可以取消注释来尝试,但如果您想尝试当前注释掉的一种,请不要忘记注释掉当前的一种),它们是同一IMessageFormatter
接口的两种不同实现,因此有效地提供了两种不同的服务,尽管在这种情况下是非常简单的服务。这仍然应该能说明问题。
Unity 容器:构造函数支持
我们使用的是与Castle Windsor相同的例子,我们使用相同的IMessageFormatter
接口,并且有两个不同的实现(SimpleMessageFormatter
和DateTimeMessageFormatter
),就像我们在上面的Castle Windsor例子中看到的。您刚才看到的配置文件显示了如何将这些类型添加到Unity容器中。
那么,我们如何使用Unity容器中的一种类型呢?嗯,这很简单。我们只需要这样做:
namespace Unity
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
UnityContainer container = new UnityContainer();
UnityConfigurationSection section = (UnityConfigurationSection)
ConfigurationManager.GetSection("unity");
section.Containers.Default.Configure(container);
// use the Current Message Formatter (see the App.Config)
var currentIMessageFormatter =
(IMessageFormatter)container.Resolve(
typeof(IMessageFormatter), "CurrentFormatter");
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1(currentIMessageFormatter));
}
}
}
从这个例子中可以看出,我们首先配置了Unity容器,然后从容器中获取所有需要的类型,然后构造一个新的Form1
对象,并将这些容器对象传递给Form1
对象的构造函数。
让我们来看看Form1
的代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Microsoft.Practices.Unity;
namespace Unity
{
public partial class Form1 : Form
{
private IMessageFormatter msgFormatter;
private int currentMessageCount = 0;
public Form1()
{
InitializeComponent();
}
public Form1(IMessageFormatter msgFormatter)
: this()
{
this.msgFormatter = msgFormatter;
}
private void btnGo_Click(object sender, EventArgs e)
{
lstItems.Items.Add(msgFormatter.FormatMessage(
String.Format("CurrentMessage {0}",
currentMessageCount++)));
}
.....
.....
}
}
从这里,我们可以看到Form1
对象在其构造函数中需要的类型已经被Unity容器解析并提供给了Form1
构造函数。很酷。
那么,当我们运行应用程序时会发生什么?嗯,与Castle Windsor示例非常相似;我们得到的格式化消息是使用当前(App.Config类型)的IMessageFormatter
接口实现的。
Unity 容器:泛型支持
这样就不错了,我们现在可以通过Unity容器使用构造函数参数来实现DI了。我们还能做什么呢?肯定还有更多。是的,确实有。与Castle Windsor允许我们使用泛型一样,Unity也可以。下面是一个例子(同样使用与Castle Windsor示例相同的接口):
这是您需要在App.Config文件中支持泛型的内容:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!-- Custom Config Sections -->
<configSections>
<section name="unity"
type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
Microsoft.Practices.Unity.Configuration" />
</configSections>
<!-- Unity Config Section -->
<unity>
<containers>
<container>
<types>
<type name="DummyObjectFormatter"
type="Unity.IGenericFormatter`1[[Unity.DummyObject,Unity]], Unity"
mapTo="Unity.DummyObjectFormatter,Unity"/>
</types>
</container>
</containers>
</unity>
</configuration>
在这种情况下,我使用Unity.DummyObject
类型作为IGenericFormatter<T>
实现使用的类型,如果您还记得,它定义如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Unity
{
public class DummyObjectFormatter :
IGenericFormatter<DummyObject>
{
public string FormatMessage(DummyObject valueIn)
{
return String.Format("The value was {0}",
valueIn.ToString());
}
}
}
我们可以看到DummyObjectFormatter
期望从Unity容器中传递一个DummyObject
。我刚才展示的配置文件节正是这样做的。
我们如何使用Unity容器来解析这一点?嗯,我们只需要这样做:
// use the IGenericFormatter Message Formatter
var dummyObjectFormatter =
(IGenericFormatter<DummyObject>)
container.Resolve(typeof(IGenericFormatter<DummyObject>),
"DummyObjectFormatter");
如果我们检查需要提供此类类型的Form1
对象的构造函数,应该很明显,我们所需要做的就是将容器获取的泛型对象传递给Form1
对象的构造函数。
public Form1(IMessageFormatter msgFormatter,
IGenericFormatter<dummyobject> actualGenTest)
: this()
{
this.msgFormatter = msgFormatter;
this.actualGenTest = actualGenTest;
}
Unity 容器:属性支持
我在使用Unity时比使用Castle时运气好一些,并且设法通过注入属性值以及构造函数值来使其工作。我确信Castle Windsor也可以做到这一点,我想我只是遗漏了一些窍门。
总之,在Unity中要使DI属性工作,您只需在属性上加上一个特殊的Unity属性,如下所示:
这是一个简单的程序文件,用于启动一个简单的表单,该表单有一个需要Unity容器提供类型的单个属性:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.Configuration;
using System.Configuration;
namespace UnityPropResolution
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
UnityContainer container = new UnityContainer();
UnityConfigurationSection section = (UnityConfigurationSection)
ConfigurationManager.GetSection("unity");
section.Containers.Default.Configure(container);
Application.Run(container.Resolve<Form1>());
}
}
}
这是非常简单的表单,请特别注意Unity [Dependency]
属性的使用:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Microsoft.Practices.Unity;
namespace UnityPropResolution
{
public partial class Form1 : Form
{
private IMessageFormatter currentFormatter;
private int currentMessageCount = 0;
[Dependency("CurrentMessageFormatter")]
public IMessageFormatter CurrentFormatter
{
get { return currentFormatter; }
set { currentFormatter = value; }
}
public Form1()
{
InitializeComponent();
}
private void btnDoIt_Click(object sender, EventArgs e)
{
MessageBox.Show(CurrentFormatter.FormatMessage(
String.Format("CurrentMessage {0}",
currentMessageCount++)));
}
}
}
如果我们查看运行时的效果,可以看到CurrentFormatter
被Unity容器成功解析了。
快速总结
我相当喜欢Unity和Castle Windsor;我花了一些时间研究了其他框架(如下面列出的),但我倾向于我在这里讨论的两个。恕我直言,Unity和Castle Windsor都是优秀的产品,它们很好地填补了IOC/DI的空白,所以如果您发现自己想尝试一下,我推荐其中任何一个。
其他值得提及的框架
- Spring .NET:一个相当成熟(但我发现相当棘手)的框架,涵盖了各种很酷的东西,例如:AOP/持久化/IOC/DI等。
- Pico .NET:一个独立的IOC容器。
- Lin Fu:这是一个提供DynamicProxy/IOC等功能的框架;作者是一位非常有才华的Philip Laureano,他在这里的CodeProject上发布了所有源代码。他非常非常聪明,他的想法非常值得一看。
就是这样
我说的就这些了。希望这篇文章对您有所帮助。如果您喜欢,能否请您随手投个票或留言?谢谢。