65.9K
CodeProject 正在变化。 阅读更多。
Home

DI/IOCs

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (30投票s)

2009年1月2日

CPOL

13分钟阅读

viewsIcon

96213

downloadIcon

759

对依赖注入和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

CastleWindsorIOCUnityIOC项目都旨在演示完全相同的问题。它们旨在演示的问题非常简单。有一个名为IMessageFormatter的单一接口,它有两个实现:DateTimeMessageFormatterSimpleMessageFormatter,可以通过在配置文件中提供正确的设置来使用。

还有一个如何使用IGenericFormatter<T>泛型接口的示例,其实现位于一个名为DummyObjectFormatter的类中。

还有一个简单的FormForm1)对象,它需要一个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接口,它有两种不同的实现(SimpleMessageFormatterDateTimeMessageFormatter)。您刚才看到的配置文件显示了如何将这些类型添加到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 

          &lt;type name="CurrentFormatter" 
              type="Unity.IMessageFormatter,Unity"
              mapTo="Unity.SimpleMessageFormatter,Unity"/&gt;
            -->
          
          <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接口,并且有两个不同的实现(SimpleMessageFormatterDateTimeMessageFormatter),就像我们在上面的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上发布了所有源代码。他非常非常聪明,他的想法非常值得一看。

就是这样

我说的就这些了。希望这篇文章对您有所帮助。如果您喜欢,能否请您随手投个票或留言?谢谢。

© . All rights reserved.