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





5.00/5 (35投票s)
通过一个简单的例子学习依赖注入的教程
目录
引言
控制反转 (IOC) 和依赖注入 (DI) 相辅相成,使我们的应用程序更加松耦合且易于扩展。强烈建议在开发时遵循 SOLID 原则。本文不会过于理论化,而是通过一个教程展示紧耦合应用程序存在的问题,如何使应用程序松耦合并通过依赖注入实现控制反转,以及使用 Unity 框架解决三层/N 层项目架构中解析依赖的依赖的问题。
问题
在三层架构中工作时,可能会出现应用程序的较高层模块/层/类依赖于较低层模块的情况。如果我们考虑一个基本简单的例子,在一个三层架构中,应用程序可以包含表示层、业务层和数据访问层。表示层与业务层通信,业务层又与数据访问层通信。要在表示层调用业务层的方法,需要在表示层创建一个业务层类的对象,然后调用所需的方法。要调用数据访问层中的类的该方法,业务层类需要创建数据访问层中类的对象,然后调用该方法。业务层与数据访问层通信,因为由于架构约束和灵活性,我们不希望表示层直接与数据访问层通信,如下所示。
考虑到本教程的读者已经知道如何解析模块的依赖,因此需要表示层本身不创建业务层对象,而是将此责任委托给任何第三方容器,并在需要实例时,为表示层创建该对象。例如,可以通过该对象访问注入的依赖项和业务层的方法。业务层与数据访问层之间的通信也是如此。这时 Unity 就派上用场了。Unity 在此对象创建和依赖解析中充当容器。我们的问题略有不同。使用表示层的 Unity,我们可以解析业务层的依赖,但数据访问层呢?我们是否需要在业务层中再次编写一个容器?实现这一点的一种方法已经在我的一篇之前的文章中进行了说明,其中我们可以利用 MEF(托管可扩展性框架),但在本教程中,我们将使用一种非常简单的方法来做到这一点。让我们先编写一些代码来展示问题,然后逐步解决。
创建无 DI 的应用程序
由于本文的重点是通过简单的方式解释 Unity 如何用于解析依赖的依赖,因此我们将不创建一个大型应用程序,而是一个小型三层控制台应用程序,每层只有一个类,每个类有一个方法,以作为概念验证。您可以创建 MVC 或任何其他应用程序,或者将此解决方案用作任何大型应用程序的 POC。
步骤 1
打开 Visual Studio。创建一个名为 Presentation
的控制台应用程序,添加一个名为 Business
的类库,再添加一个名为 Data
的类库。这样,我们就用这些类库来分隔我们的层,其中 Business 类库用于业务层,Data 类库用于数据访问层。解决方案应该如下所示。我将解决方案命名为“WithoutDI
”。
从 Business
和 Data
类库中删除名为 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
项目的 DataClass
的 GetData
方法,我们首先需要在 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();
}
}
}
当我们运行应用程序时,我们会得到所需的输出,如下所示:
这意味着我们的数据来自 DataClass
的 GetData()
方法。我们的应用程序运行正常,但问题是它是否遵循 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
构造函数中,并且我们在运行时将有一个对象来调用 DataClass
的 GetData
方法。因此,我们的 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 来注册传入的任何类型的实例。我们可以将其特定于我们的类和接口,但最好是通用的,以便在未来能够处理我们应用程序中的任何新类型。
要定义和注册我们的具体类型,例如 IBusiness
和 Business
类,请向 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
”关键字,这个类主要负责初始化和应用程序启动。
简而言之,我们做了以下工作:
- 引入了通用的
DependencyInjector
类来处理注册/解析类型。 - 引入了
Bootstrapper
类,该类在其Init()
方法中调用DependencyInjector
类的方法来注册具体类型。 - 在 Program.cs 的
Main
方法中调用Bootstrapper
类的Init
方法,以便在应用程序启动时注册和解析类型。 - 调用了
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}
”
是的,您说得对。我们已经处理了将 BusinessClass
与 IBusiness
注册。我们处理了 Initiator
类实例的解析,但 DataClass
呢?请记住,当我们尝试获取 DataClass
实例时,我们也从 BusinessClass
构造函数中删除了“new
”,但我们尚未注册该类型,也尚未解析它以备使用。
解决方案是我们也需要为 DataClass
注册类型,以便在需要时获得其实例。但如何做到呢?由于这会违反我们的分层架构,因此我们无法在表示层中访问或引用 Data
项目。所以,唯一的地方是 Business
项目。但是,如果我们像在表示层中所做的那样全部完成,这将没有意义,并且我们会出现代码重复和应用程序中有两个引导程序。一种方法是我在之前的文章中已经解释过的 MEF,但那种方法有点复杂。让我们探索如何在不引用表示项目中的 Data
项目、不进行代码重复、没有多个混乱/难以管理的引导程序且不违反 SOLID/耦合的情况下克服这种情况。
使用 Unity 扩展解析依赖的依赖
Unity 通过 UnityContainerExtension
s 提供了这种灵活性,可以在不破坏结构的情况下解析依赖的依赖,如下所示:
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
)的解析。
总结
让我们快速总结一下我们在应用程序和整个教程中所做的工作:
- 我们创建了一个包含表示层、业务层和数据访问层在内的三层基本架构。该架构是紧耦合的,并违反了 SOLID。
- 我们引入了 Unity 来解决表示层和业务层之间的依赖关系。之后,我们使用构造函数注入通过接口注入依赖项。
- 我们使用 Unity 扩展解析了依赖的依赖,以确保我们的架构不被破坏。
- 我们摆脱了“
new
”关键字,并将对象创建的责任委托给了 Unity 容器。 - 我们通过依赖注入实现了控制反转。
- 每个类都有单一职责,并且对扩展开放而不是修改。
- 我们借助接口实现了抽象。
结论
在本教程中,我们尝试通过一个简单的例子来学习依赖注入。所取的例子非常基础,但该概念可以应用于 MVC、Web API 或任何企业级应用程序,以解决依赖关系并通过依赖注入实现控制反转。我们还使用 Unity 扩展解析了依赖的依赖。还有其他解析依赖的方法,例如属性注入或服务定位器。我们在应用程序中使用了构造函数注入。可以使用 Unity 以外的其他容器。我使用了 Unity,因为我对它更熟悉。希望您喜欢这篇文章。编码愉快!😉
历史
- 2018 年 3 月 13 日:初始版本