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

使用 Unity 进行依赖注入:解析依赖的依赖

starIconstarIconstarIconstarIconstarIcon

5.00/5 (35投票s)

2018 年 3 月 13 日

CPOL

15分钟阅读

viewsIcon

103251

downloadIcon

1870

通过一个简单的例子学习依赖注入的教程

目录

引言

控制反转 (IOC) 和依赖注入 (DI) 相辅相成,使我们的应用程序更加松耦合且易于扩展。强烈建议在开发时遵循 SOLID 原则。本文不会过于理论化,而是通过一个教程展示紧耦合应用程序存在的问题,如何使应用程序松耦合并通过依赖注入实现控制反转,以及使用 Unity 框架解决三层/N 层项目架构中解析依赖的依赖的问题。

问题

在三层架构中工作时,可能会出现应用程序的较高层模块/层/类依赖于较低层模块的情况。如果我们考虑一个基本简单的例子,在一个三层架构中,应用程序可以包含表示层、业务层和数据访问层。表示层与业务层通信,业务层又与数据访问层通信。要在表示层调用业务层的方法,需要在表示层创建一个业务层类的对象,然后调用所需的方法。要调用数据访问层中的类的该方法,业务层类需要创建数据访问层中类的对象,然后调用该方法。业务层与数据访问层通信,因为由于架构约束和灵活性,我们不希望表示层直接与数据访问层通信,如下所示。

考虑到本教程的读者已经知道如何解析模块的依赖,因此需要表示层本身不创建业务层对象,而是将此责任委托给任何第三方容器,并在需要实例时,为表示层创建该对象。例如,可以通过该对象访问注入的依赖项和业务层的方法。业务层与数据访问层之间的通信也是如此。这时 Unity 就派上用场了。Unity 在此对象创建和依赖解析中充当容器。我们的问题略有不同。使用表示层的 Unity,我们可以解析业务层的依赖,但数据访问层呢?我们是否需要在业务层中再次编写一个容器?实现这一点的一种方法已经在我的一篇之前的文章中进行了说明,其中我们可以利用 MEF(托管可扩展性框架),但在本教程中,我们将使用一种非常简单的方法来做到这一点。让我们先编写一些代码来展示问题,然后逐步解决。

创建无 DI 的应用程序

由于本文的重点是通过简单的方式解释 Unity 如何用于解析依赖的依赖,因此我们将不创建一个大型应用程序,而是一个小型三层控制台应用程序,每层只有一个类,每个类有一个方法,以作为概念验证。您可以创建 MVC 或任何其他应用程序,或者将此解决方案用作任何大型应用程序的 POC。

步骤 1

打开 Visual Studio。创建一个名为 Presentation 的控制台应用程序,添加一个名为 Business 的类库,再添加一个名为 Data 的类库。这样,我们就用这些类库来分隔我们的层,其中 Business 类库用于业务层,Data 类库用于数据访问层。解决方案应该如下所示。我将解决方案命名为“WithoutDI”。

BusinessData 类库中删除名为 Class1.cs 的默认类。

步骤 2

现在,在 Business 项目中添加一个名为 BusinessClass 的类,在 Data 项目中添加一个名为 DataClass 的类。在 DataClass 中添加一个名为 GetData 的方法,并从此方法返回一个 string。因此,该类的实现如下所示:

namespace Data
{
  public class DataClass
  {
    public string GetData()
    {
      return "From Data";
    }
  }
}

步骤 3

现在我们的要求是将来自 GetData 方法的数据获取到我们的表示层并在控制台上显示。直接的方法是从表示层的 Program.cs 调用 Business 类的方法,该方法反过来会调用 DataClass 中的 GetData 方法。要实现这一点,我们首先需要在 Presentation 控制台应用程序中添加 Business 项目的引用,并在 Business 类库中添加 Data 项目的引用。

步骤 4

现在,要在 BusinessClass 中访问 Data 项目的 DataClassGetData 方法,我们首先需要在 BusinessClass 中获得 DataClass 的实例。我们在 BusinessClass 构造函数中创建实例,然后 Business 类中应该有一个方法,该方法利用此 DataClass 实例来调用 GetData 方法以返回 string。因此,实现如下:

using Data;
namespace Business
{
  public class BusinessClass
  {
    DataClass _dataClass;
    public BusinessClass()
    {
       _dataClass = new DataClass();
    }
    public string GetBusinessData()
    {
      return _dataClass.GetData();
    }
  }
}

步骤 5

现在,要从表示层调用 Business 类的 GetBusinessData 方法,我们再次需要在表示层中创建 BusinessClass 类的实例并调用该方法。因此,在 main 方法或 Program.cs 中创建一个 BusinessClass 实例,并通过该实例调用 GetBusinessData,如下所示:

using System;

namespace Presentation
{
  class Program
  {
    static void Main(string[] args)
    {
      Business.BusinessClass businessClass = new Business.BusinessClass();
      string data = businessClass.GetBusinessData();
      Console.WriteLine(data);
      Console.ReadLine();
    }
  }
}

当我们运行应用程序时,我们会得到所需的输出,如下所示:

这意味着我们的数据来自 DataClassGetData() 方法。我们的应用程序运行正常,但问题是它是否遵循 SOLID 原则?我的应用程序是否松耦合且自治,我的应用程序是否健壮?如果我们仔细观察并尝试回答这些问题,答案是 NO。

应用程序违反了 **S**ingle Responsibility Principle(单一职责原则),因为类还承担了创建依赖类对象的额外工作。

应用程序违反了 **O**pen Close principle(开闭原则),因为如果类中的构造函数实现发生更改,调用类将不得不修改其代码以适应此更改,这可能会破坏应用程序或并不总是可行。

应用程序间接违反了 **I**nterface Segregation Principle(接口隔离原则),因为应用程序的可测试性非常差,我们没有为充当合同的类定义接口。

应用程序违反了 **D**ependency Inversion principle(依赖倒置原则),因为类依赖于其他类的实例,并在类中直接创建实例。想象一下 DataClass 的实例创建失败的情况,由于这个原因,Business 类的构造函数也将无法初始化并抛出错误。这完全不可接受。

因此,五项原则中的四项被违反了。此外,应用程序在单元测试方面是不可测试的。应用程序是紧耦合的。让我们尝试逐一解决这些问题,并在 Unity 框架的帮助下进行,同时继续了解如何解析依赖的依赖。

在应用程序中引入 Unity

是时候在应用程序中引入 Unity 了。Unity Framework 是一个 Nuget 包,所以我们将通过程序包管理器控制台安装它。打开 Visual Studio 中的 Tools,导航到 Nuget Package Manager,然后打开 Package Manager Console,如下图所示:

打开程序包管理器控制台后,键入命令 Install-Package Unity 以获取最新版本的 Unity 框架。确保在控制台中将 Presentation 项目选为默认项目。键入命令后按 **Enter**,Unity 包将为 presentation 项目安装。我获得的最新版本是 5.7.3。它可能会有所不同,取决于您何时基于发布的最新版本进行实现。

应用程序中会创建一个 packages.config 文件,其中会引用此 Unity 包,并且该包本身会下载到文件系统的 packages 文件夹中,并自动引用到 Presentation 项目。

通过 Unity 进行依赖注入

让我们对 Presentation 层进行一些修改,而不是在 Main 方法中调用 BusinessClass 方法,而是添加一个名为 Initiator 的类,并从那里调用方法。我们本可以早点做到这一点,但我们错过了,所以现在就做。这只是为了更清楚地理解我们如何解析依赖项。因此,添加一个名为 Initiator 的类,并添加以下代码:

using Business;
namespace Presentation
{
  public class Initiator
  {
    BusinessClass _businessClass;
    public Initiator()
    {
      _businessClass = new BusinessClass();
    }
    public string FetchData()
    {
      return _businessClass.GetBusinessData();
    }
  }
}

Program.cs 的代码如下:

using System;

namespace Presentation
{
  class Program
  {
    static void Main(string[] args)
    {
      Initiator initiator = new Initiator();
      string data = initiator.FetchData();
      Console.WriteLine(data);
      Console.ReadLine();
    }
  }
}

输出和逻辑保持不变,我们只是将 Program.cs 中的一些部分移到了 Initiator 类,并且现在 Main 方法使用此 Initiator 类实例来获取数据。

所以,现在这里的问题是,Program.cs Main() 方法创建 Initiator 类的实例,Initiator 类创建 BusinessClass 的实例,而 Business 类创建 DataClass 的实例来获取数据。让我们将此实例创建的责任交给别人,例如 Unity,并通过接口在定义的构造函数中注入依赖项。是的,我们现在需要创建接口并定义相应的方法。让我们一步一步来。

步骤 1

Data 项目中创建一个名为 IData 的接口,并在其中定义 GetData () 方法,如下所示:

namespace Data
{
  public interface IData
  {
    string GetData();
  }
}

现在,在 DataClass 中实现此接口,如下所示:

namespace Data
{
  public class DataClass : IData
  {
    public string GetData()
    {
      return "From Data";
    }
  }
}

步骤 2

现在转到 Business 项目,定义一个名为 IBusiness 的接口,并在其中定义 GetBusinessData() 方法,如下所示:

namespace Business
{
  public interface IBusiness
  {
    string GetBusinessData();
  }
}

BusinessClass 中实现此接口,如下所示:

using Data;
namespace Business
{
  public class BusinessClass
  {
    DataClass _dataClass;
    public BusinessClass()
    {
       _dataClass = new DataClass();
    }
    public string GetBusinessData()
    {
      return _dataClass.GetData();
    }
  }
}

现在,假设 Business 类将不再承担对象创建的责任,并且此依赖项将在运行时注入到 BusinessClass 构造函数中,并且我们在运行时将有一个对象来调用 DataClassGetData 方法。因此,我们的 Business 类看起来如下:

using Data;

namespace Business
{
  public class BusinessClass : IBusiness
  {
    IData _dataClass;

    public BusinessClass(IData dataClass)
    {
      _dataClass = dataClass;
    }
   public string GetBusinessData()
    {
      return _dataClass.GetData();
    }
  }
}

这相当直接。我们声明 IData 实例局部变量,并在运行时期望在 BusinessClass 的构造函数中获得 IData 实例,我们将其分配给我们的局部变量,并在 GetBusinessData() 方法中,我们使用此局部变量,假设它已提前初始化,我们调用 GetData() 方法。请注意,此处未使用的“new”关键字来创建实例,现在 BusinessClass 也不承担创建 DataClass 对象的责任。此代码仍然无法正常工作。我们还需要在表示层做一些工作。

步骤 3

移至 Presentation 项目,执行与我们在 Business 层为 DataClass 所做的事情相同的操作,但现在在 Initiator 类中为 Business 层执行。因此,我们的 Initiator 类如下所示:

using Business;
namespace Presentation
{
  public class Initiator
  {
    IBusiness _businessClass;
    public Initiator(IBusiness businessClass)
    {
      _businessClass = businessClass;
    }
    public string FetchData()
    {
      return _businessClass.GetBusinessData();
    }
  }
}

这也很容易理解。我们声明 IBusiness 实例局部变量,然后在构造函数中,我们假定我们获得了预先初始化的 business 类实例,并将其分配给我们的局部变量,并通过它调用 GetBusinessData() 方法。请注意,我们已完全从此处删除了“new”关键字,并且此类也不承担创建对象的责任。我们的工作还没有完成。我们仍然在 Program.cs 类中有 Main() 方法,该方法通过“new”关键字创建 Initiator 类的对象并调用 FetchData() 方法。我们的目标是完全摆脱这个“new”关键字,并确保类遵循单一职责原则。

步骤 4

Presentation 项目中添加一个名为 DependencyInjector 的新类,并向其中添加以下代码:

using Unity;
using Unity.Lifetime;

namespace Presentation
{
  public static class DependencyInjector
  {
    private static readonly UnityContainer UnityContainer = new UnityContainer();
    public static void Register<I, T>() where T : I
    {
      UnityContainer.RegisterType<I, T>(new ContainerControlledLifetimeManager());
    }
    public static void InjectStub<I>(I instance)
    {
      UnityContainer.RegisterInstance(instance, new ContainerControlledLifetimeManager());
    }
    public static T Retrieve<T>()
    {
      return UnityContainer.Resolve<T>();
    }
  }
}

这个 DependencyInjector 类是一个泛型类,负责解析类型和注册类型。它使用 Unity 来注册传入的任何类型的实例。我们可以将其特定于我们的类和接口,但最好是通用的,以便在未来能够处理我们应用程序中的任何新类型。

要定义和注册我们的具体类型,例如 IBusinessBusiness 类,请向 presentation 项目添加另一个名为 Bootstrapper 的类。这个类就像它的名字一样,是应用程序的 Bootstrapper,即应用程序加载时第一个解析依赖项的类。因此,添加 Bootstrapper.cs 并将以下代码添加到其中:

using Business;
namespace Presentation
{
  public static class Bootstrapper
  {
    public static void Init()
    {
      DependencyInjector.Register<IBusiness, BusinessClass>();
    }
  }
}

这个类只是调用 DependencyInjector 类的通用 Register 方法,并请求解析 BusinessClass 的依赖项,以便在需要时提供 Business 类的实例。

步骤 5

现在,在 Main 方法中,只需调用 Bootstrapper 类的此 Init() 方法来注册类型,并通过 DependencyInjector 类的 Retrieve 方法获取 Initiator 类的实例。我们需要此 Initiator 实例来调用 Initiator 类的 FetchData 方法。因此,这是我们 Program.cs 类的代码:

using System;
namespace Presentation
{
  public class Program
  {
    static void Main(string[] args)
    {
      Bootstrapper.Init();
      Initiator initiator = DependencyInjector.Retrieve<Initiator>();
      string data = initiator.FetchData();
      Console.WriteLine(data);
      Console.ReadLine();
    }
  }
}

简洁明了。请注意,我们也从这里摆脱了“new”关键字,这个类主要负责初始化和应用程序启动。

简而言之,我们做了以下工作:

  1. 引入了通用的 DependencyInjector 类来处理注册/解析类型。
  2. 引入了 Bootstrapper 类,该类在其 Init() 方法中调用 DependencyInjector 类的方法来注册具体类型。
  3. Program.csMain 方法中调用 Bootstrapper 类的 Init 方法,以便在应用程序启动时注册和解析类型。
  4. 调用了 Initiator 类中的 FetchData() 方法。

对我来说,一切看起来都很好,我们可以开始运行应用程序了。

步骤 6

通过按 F5 运行应用程序。我们假设一切都会正常工作,因为我们已经处理了依赖项,但我们得到了以下运行时错误:

当您检查 InnerException 时,错误是明显且不言自明的。它显示:“- InnerException {"The current type, Data.IData, is an interface and cannot be constructed. Are you missing a type mapping?"} System.Exception {System.InvalidOperationException}

是的,您说得对。我们已经处理了将 BusinessClassIBusiness 注册。我们处理了 Initiator 类实例的解析,但 DataClass 呢?请记住,当我们尝试获取 DataClass 实例时,我们也从 BusinessClass 构造函数中删除了“new”,但我们尚未注册该类型,也尚未解析它以备使用。

解决方案是我们也需要为 DataClass 注册类型,以便在需要时获得其实例。但如何做到呢?由于这会违反我们的分层架构,因此我们无法在表示层中访问或引用 Data 项目。所以,唯一的地方是 Business 项目。但是,如果我们像在表示层中所做的那样全部完成,这将没有意义,并且我们会出现代码重复和应用程序中有两个引导程序。一种方法是我在之前的文章中已经解释过的 MEF,但那种方法有点复杂。让我们探索如何在不引用表示项目中的 Data 项目、不进行代码重复、没有多个混乱/难以管理的引导程序且不违反 SOLID/耦合的情况下克服这种情况。

使用 Unity 扩展解析依赖的依赖

Unity 通过 UnityContainerExtensions 提供了这种灵活性,可以在不破坏结构的情况下解析依赖的依赖,如下所示:

namespace Unity.Extension
{
  public abstract class UnityContainerExtension: IUnityContainerExtensionConfigurator
  {
    protected UnityContainerExtension();

    public IUnityContainer Container { get; }
    protected ExtensionContext Context { get; }

    public void InitializeExtension(ExtensionContext context);
    public virtual void Remove();
    protected abstract void Initialize();
  }
}

这个类是 Unity.Abstractions DLL 的一部分,包含一个名为 Initialize()abstract 方法。Abstract 方法意味着我们可以覆盖此方法并编写自定义初始化逻辑。所以,让我们这样做。

Business 项目中创建一个名为 DependencyOfDependencyExtension 的类。您可以根据自己的选择命名该类。为了便于理解,我在本例中使用了此名称。以与表示项目相同的方式将 Unity 包安装到 Business 项目中。不要忘记在程序包管理器控制台中选择 Business 作为默认项目,如下图所示:

步骤 1

将 Unity 添加到项目后,就像在表示层中所做的那样,向我们新创建的 DependencyOfDependencyExtension 类中添加以下命名空间:

using Data;
using Unity;
using Unity.Extension;

现在,让该类继承自 Unity.Extension 命名空间中的 UnityContainerExtension 类,该类位于 Unity.Abstractions 程序集中,并覆盖其 Initialize 方法,如下所示,以将 DataClass 类型与 IData 注册:

using Data;
using Unity;
using Unity.Extension;

namespace Business
{
  public class DependencyOfDependencyExtension : UnityContainerExtension
  {
    protected override void Initialize()
    {
      Container.RegisterType<IData,DataClass>();
    }
  }
}

步骤 2

现在,我们必须让我们的表示层了解此扩展。因此,移至表示层,并在 DependencyInjector 类中,添加一个名为 AddExtension() 的新泛型方法来添加新扩展,如下所示:

public static void AddExtension<T>() where T : UnityContainerExtension
    {
      UnityContainer.AddNewExtension<T>();
    }

不要忘记在此类中添加 using Unity.Extension;namespace 来访问 UnityContainerExtension 类。将此类完成如下:

using Unity;
using Unity.Extension;
using Unity.Lifetime;

namespace Presentation
{
  public static class DependencyInjector
  {
    private static readonly UnityContainer UnityContainer = new UnityContainer();
    public static void Register<I, T>() where T : I
    {
      UnityContainer.RegisterType<I, T>(new ContainerControlledLifetimeManager());
    }
    public static void InjectStub<I>(I instance)
    {
      UnityContainer.RegisterInstance(instance, new ContainerControlledLifetimeManager());
    }
    public static T Retrieve<T>()
    {
      return UnityContainer.Resolve<T>();
    }
    public static void AddExtension<T>() where T : UnityContainerExtension
    {
      UnityContainer.AddNewExtension<T>();
    }
  }
}

步骤 3

我们几乎完成了,现在在 Bootstrapper 类的 Init 方法中为此扩展添加具体类型,如下所示:

using Business;
namespace Presentation
{
  public static class Bootstrapper
  {
    public static void Init()
    {
      DependencyInjector.Register<IBusiness, BusinessClass>();
      DependencyInjector.AddExtension<DependencyOfDependencyExtension>();
    }
  }
}

工作完成,只需运行应用程序,我们就会看到所需的输出,如下所示:

这样,我们就解决了依赖的依赖,即表示层对依赖(BusinessClass)的依赖(DataClass)的解析。

总结

让我们快速总结一下我们在应用程序和整个教程中所做的工作:

  1. 我们创建了一个包含表示层、业务层和数据访问层在内的三层基本架构。该架构是紧耦合的,并违反了 SOLID。
  2. 我们引入了 Unity 来解决表示层和业务层之间的依赖关系。之后,我们使用构造函数注入通过接口注入依赖项。
  3. 我们使用 Unity 扩展解析了依赖的依赖,以确保我们的架构不被破坏。
  4. 我们摆脱了“new”关键字,并将对象创建的责任委托给了 Unity 容器。
  5. 我们通过依赖注入实现了控制反转。
  6. 每个类都有单一职责,并且对扩展开放而不是修改。
  7. 我们借助接口实现了抽象。

结论

在本教程中,我们尝试通过一个简单的例子来学习依赖注入。所取的例子非常基础,但该概念可以应用于 MVC、Web API 或任何企业级应用程序,以解决依赖关系并通过依赖注入实现控制反转。我们还使用 Unity 扩展解析了依赖的依赖。还有其他解析依赖的方法,例如属性注入或服务定位器。我们在应用程序中使用了构造函数注入。可以使用 Unity 以外的其他容器。我使用了 Unity,因为我对它更熟悉。希望您喜欢这篇文章。编码愉快!😉

历史

  • 2018 年 3 月 13 日:初始版本
© . All rights reserved.