依赖反转原则 (DIP)、控制反转 (IoC)、依赖注入 (DI) 和 IoC 容器的集合






4.96/5 (140投票s)
DIP、IoC、DI、IoC 容器及其在实际工作中的使用描述
目录
- 引言
- 背景
- 依赖反转原则 (DIP)
- 融会贯通 (DIP、IoC 和 DI)
- 控制反转(IoC)
- 依赖注入 (DI)
- 控制反转容器 (IoC 容器)
- 使用 Ninject IoC 容器
- 结论
- 历史
引言
欢迎阅读我的文章。在这里,我将尝试描述 DIP、IoC、DI 和 IoC 容器。大多数时候,初级开发人员在 DIP、IoC、DI 和 IoC 容器方面会遇到问题。他们将所有概念混淆,难以区分它们之间的差异,也不知道为什么要使用它们。另一方面,许多人在不知不觉中使用 DI、IoC,也不知道它解决了什么问题。关于这个主题的文章、博客很多,但很少将它们放在一起。在这里,我试图将所有这些概念一起描述。我希望读者在阅读我的文章后,能够区分 DIP、IoC、DI 和 IoC 容器/框架之间的差异,并了解何时以及如何使用它们。我还希望读者在阅读我的文章后,能够创建自己的 IoC 容器。在深入细节之前,让我用简单的句子描述一下 DIP、IoC、DI 和 IoC 容器。
依赖反转原则 (DIP) | 软件架构中使用的原则 |
控制反转(IoC) | 用于反转流程、依赖项和接口的模式 |
依赖注入 (DI) | IoC 的实现,用于反转依赖项 |
IoC 容器 | 用于进行依赖注入的框架。它有助于映射依赖项,管理对象的创建和生命周期。 |
背景
DIP 是一种软件设计原则,IoC 是一种软件设计模式。让我们看看什么是软件设计原则和模式。
- 软件设计原则:原则为我们提供指导方针。原则告诉我们什么是对的,什么是错的。它不告诉我们如何解决问题。它只是提供一些指导方针,以便我们能够设计出好的软件并避免不良设计。一些原则是 DRY、OCP、DIP 等。
- 软件设计模式:模式是在给定软件设计上下文中,针对常见问题的一种通用可重用解决方案。一些模式是工厂模式、装饰器模式等。
现在我们已经具备了深入探讨的一切条件。
所以原则定义了好与坏。因此我们可以说,如果在软件设计过程中遵循原则,那么它将是好的设计。如果我们不遵循原则,那么我们可能会遇到问题。现在我们只关注依赖反转原则(DIP),它是 **SOLID** 原则中的 **D**。
依赖反转原则 (DIP)
高层模块不应依赖于低层模块定义的接口,而应由高层模块定义接口,低层模块实现该接口。
让我们通过一些例子来理解 DIP
让我们尝试用便携式充电器或设备的简单例子来描述上述原则。假设您有相机、手机和其他设备。这些设备使用数据线连接电脑或充电器。一个简单的插孔或端口用于连接电脑或充电器,对吗?现在,如果问您是谁定义了端口或插孔;是数据线还是您的设备?
你肯定会回答设备。端口会根据设备而变化。所以从这个故事中,我们发现端口并不能定义设备是什么,但设备定义了端口或插孔是什么。
所以软件模块也是类似的。高层模块定义接口,低层模块实现该接口,而低层模块不定义接口,就像插孔不定义设备一样。
让我们再次根据 Bob Martin 的定义来考虑 DIP
A. 高级模块不应依赖于低级模块。两者都应依赖于抽象。
B. 抽象不应依赖于细节。细节应依赖于抽象。
所以从原则上我们可以说,高层模块不应该依赖于低层模块。两者都依赖于抽象。抽象不应该依赖于低层。
让我们通过一些例子来理解 DIP
案例 1:依赖未反转(高层模块依赖于低层接口)
从图中我们发现高层依赖于低层接口。因此高层类需要考虑所有接口。当新的低层类出现时,高层类需要再次更改,这会增加维护的复杂性并违反开闭原则。
案例 2:依赖反转
现在看图。在这里,高层类定义了接口,高层类不直接依赖于低层类。低层类实现高层类定义的接口,因此当新的实现到来时,高层类不需要改变。
案例 3:不使用 DIP 的复制程序
考虑上面的例子
- 首先,忽略虚线对象。您有一个复制程序,负责从键盘获取输入并写入文本文件。您当前的复制程序可以同时处理这两者,因此它不会产生问题。
- 现在考虑过了一段时间,你的老板要求你开发你的复制程序,使其能够将读取的数据存储到数据库。那么你该怎么办?你将再次修改复制程序,使其能够写入数据库。这将增加你程序的复杂性。
案例 4:使用 DIP 的复制程序
如果我们考虑上面的例子,我们会发现复制程序依赖于由复制程序定义的读取器接口和写入器接口。因此,它不直接依赖于扫描器读取、打印机写入等低级类。结果,当有新需求时,我们不需要更改复制程序,而且我们的复制程序是独立的。
我认为 DIP 已经比较清楚了。现在让我们看看遵循 DIP 的好处和不遵循 DIP 的问题。如果我们不遵循软件设计原则,那么我们的软件设计将是糟糕的设计。现在考虑一个我们没有遵循依赖反转原则 (DIP) 的软件设计。由于我们没有遵循 DIP,那么我们就会有问题。让我们看看。
如果我们不遵循 DIP 将面临的问题
- 系统将僵化:更改系统的一部分会影响系统中的许多其他部分,这将很困难。
- 系统将变得脆弱:当我们做出更改时,系统中意想不到的部分将会崩溃。
- 系统或组件将无法移动:很难在另一个应用程序中重用它,因为它无法从当前应用程序中分离出来。等等...
遵循 DIP 的好处
首先,我们可以解决上述问题。这意味着我们的系统将是松耦合的、独立的、模块化的、可测试的等等。由于 DIP 是一种原则,它并没有说明如何解决问题。如果我们想知道如何解决问题,那么我们就必须转向控制反转。
控制反转(IoC)
DIP 并没有告诉我们如何解决问题,但控制反转定义了一些方法,以便我们能够遵循 DIP。这是您在软件开发中可以实际应用的东西。关于 IoC 有很多定义。在这里我只是尝试给出一个简单的定义,以便我们能够轻松理解。
什么是 IoC
IoC 帮助我们应用 DIP。简单来说,IoC 就是通过切换控制者来反转某些事物的控制权。系统中的特定类或另一个模块将负责从外部创建对象。控制反转意味着我们正在改变常规的控制方式。
IoC 和 DIP
DIP 说高层模块不应该依赖于低层模块,两者都应该依赖于抽象。IoC 是一种提供抽象的方式。一种改变控制的方式。IoC 提供了一些实现 DIP 的方法。如果你想使高层模块独立于低层模块,那么你必须反转控制,以便低层模块不控制接口和对象的创建。最后,IoC 提供了一些反转控制的方法。
拆分 IoC
我们可以通过以下方式分解 IoC。(稍后将提供详细描述。)
- 接口反转:反转接口
- 流程反转:反转控制流,这是 IoC 的基本思想,类似于“别打电话给我们,我们会打给你。”
- 创建反转:这是开发人员最常用的方法。我们将在介绍 DI 和 IoC 容器时使用它。
融会贯通 (DIP、IoC 和 DI)
我这里不详细阐述。只要考虑上图,所有事物如何完美契合。这就是所有事物如何契合在一起的视图。DI 不仅仅是依赖创建的一种方式,这就是我使用“……”的原因。实现依赖创建的方法有很多。在这里,我只对 DI 感兴趣。这就是为什么我在图中显示了它,而其他的都是点缀的。顶部是 DIP,它是一种软件设计方式。它没有说明如何创建独立模块。IoC 提供了一些应用 DPI 原则的方法。IoC 不提供具体的实现。它提供了一些方法,以便我们可以反转控制。如果我们要使用绑定反转或依赖创建来反转控制,那么我们可以通过实现依赖注入(DI)来实现。
接口反转
接口反转就是接口的反转。考虑一个 Reader 应用程序。这个 Reader 应用程序从文本文件读取。
现在考虑这个应用程序也从 pdf 和 word 文件中读取
创建太多接口并不意味着我在实现 IoC 或遵循 DPI。使用太多接口有什么好处?这里我们没有任何好处,因为每次我们都必须更改我们的 reader 类,并且 reader 类必须维护所有接口。所以最终我们并没有受益。现在我们有一种叫做接口反转的方法。让我们反转接口。
通过考虑上面的例子,我们从较低级别的类中移除了所有接口,并创建了一个由读取器类定义的单一接口。我们在这里所做的只是反转接口。现在我们有很多好处。我们不再需要更改读取器类,并且读取器类是独立的。现在将上面的事物视为一个提供者模型。
流程反转
让我们看看流程反转。流程反转是 IoC 的简单反转,也是其骨干。如果你考虑正常流程,你会发现它是程序性的。
正常流程
考虑一个命令行程序,它首先询问你的名字,然后你输入你的名字,它会再次询问你的年龄,然后你提供你的年龄。这里发生了什么?你发现你的命令行应用程序一个接一个地执行,这意味着基本流程是程序性的。它一步一步地进行。
反转流程
现在让我们尝试反转流程。现在我们可以考虑一个 GUI。假设我们的 GUI 由两个输入框组成,一个用于用户名,另一个用于用户年龄。请看下图
现在你看到你可以提供你的名字和年龄,而无需遵循流程。如果你愿意,你可以先输入年龄,再输入名字。所以你不需要依赖基本流程。另一方面,当你按下保存按钮时,你的程序会保存信息。在命令行程序中,你是在要求先做这个,再做那个,但在 GUI 中,流程是由用户控制的。
创建反转
这是最常用的控制反转方法,我们将在 IoC 容器中使用它。现在考虑以下内容。
通常情况下,我们如何创建一个对象
Class A
{
YourClass yourObject = new YourClass();
}
因此,您正在创建依赖于另一个类的对象,这使得系统紧密耦合。在这里,您是在当前类内部创建对象。
现在考虑接口反转
Class A
{
IYourInterface yourObject = new YourClass();
}
即使我们正在使用接口反转,我们仍然在类内部创建对象。所以对象的创建仍然存在依赖关系。那么什么是创建反转呢?为了打破依赖关系,我们必须做什么?让我们看看...
反转控制/创建反转
在类外部创建它们将使用的对象。我们从类外部创建对象,因此高层类不直接依赖于低层类。现在让我们看看为什么需要反转控制。
假设你有一个 GUI,你想在屏幕上放置一个按钮。你的按钮有多种设计,所以你的 UI 根据 UserSettings 显示按钮。如果以正常方式操作,我们的代码会是怎样?让我们看看。
Button btn;
switch (UserSettings.ButtonStyle)
{
case "Metro": btn = new MetroButton();
break;
case "Office2010": btn = new Office2007Button();
break;
case "Fancy": btn = new FancyButton();
break;
}
所以从代码来看,我们的 UI 将根据用户设置显示不同风格的按钮。现在过了一段时间,新的风格出现了,那么你又必须更改 UI 代码。如果出现 20 种风格,那么我们必须更改 20 次。正如创建反转所说,我们必须在类外部创建对象。现在让我们尝试遵循这一点。
现在假设我们有一个 `FactoryClass`(我们可以通过阅读工厂模式了解更多),它负责根据用户设置提供按钮对象。这个工厂类负责对象的创建。现在,让我们再次看看在实现 `ButtonFactory` 类之后我们的 UI 类。在这里,我没有描述如何创建 `Button` 工厂类,但你稍后会找到详细信息。
所以新的 UI 有以下代码
Button btn = ButtonFactory.CreateButton();
因此,我们的按钮依赖于工厂类,当有新样式出现时,我们无需更改 UI 代码。在这里,对象创建是在 UI 类外部完成的。通过这种方式,我们正在反转对象的创建。
创建反转的类型
工厂模式
如果你查看上面的例子,你会发现我是在 UI 类之外管理对象的创建。在工厂模式中,我们这样做:
Button btn = ButtonFactory.CreateButton();
服务定位器
Button btn = ServiceLocator.Create(IButtonControl);
在服务定位器模式中,我们传递接口,并且服务定位器将提供所请求接口的相应实现。在这里,我不会详细描述,因为我只专注于描述依赖注入 (DI)。
依赖注入 (DI)
在依赖注入中,我们传递依赖项。考虑以下简单代码
Button btn = GetButtonInstanceBasedOnUserSettings();
OurUi ourUI = new OurUi(btn);
在这里,我们在创建 UI 时传递了依赖项。在 DI 中,核心思想是传递依赖项。DI 不仅仅是构造函数注入。在下一节中,我将详细描述 DI。
更多…
还有更多创建反转的方法。
因此,总而言之,无论何时您将依赖项以及依赖项的绑定从类中取出,您都在反转控制,这被视为控制反转 (IoC)。
依赖注入 (DI)
大多数时候,人们会将 DI 与 IoC 混淆。IoC 描述了反转控制的不同方式。另一方面,DI 通过传递依赖项来反转控制。所以在这里,我将尝试描述以下内容
- 什么是依赖注入 (DI)
- DI 的类型
- 构造函数注入
- Setter 注入
- 接口注入
什么是依赖注入
IoC 的一种类型,我们将依赖项的创建和绑定从依赖它的类中移出。通常,对象在依赖类内部创建并绑定在依赖类内部。在 DI 中,它是在依赖类外部完成的。让我们考虑一个例子
假设您带着饭盒去办公室吃早餐。所以您带了所有早餐需要的东西。因此,您通过饭盒管理早餐。这类似于在类内部创建对象,从某种意义上说,您正在管理您需要的东西。
现在考虑另一个例子。在您的办公室里,会提供早餐。您不需要随身携带饭盒。那么会发生什么呢?您仍然可以吃早餐,但现在是由办公室提供的。这类似于 DI,其中所需的™对象在类外部提供。
依赖注入的类型
构造函数注入
这是最常见的 DI 形式。通过构造函数将依赖项传递给依赖类。注入器创建依赖项并通过构造函数传递依赖项。让我们考虑以下示例。我们有一个 `PurchaseBl` 类,它负责执行保存操作并依赖于 `IRepository`。
public class PurchaseBl
{
private readonly IRepository _repository;
//Constructor
public PurchaseBl(IRepository repository)
{
_repository = repository;
}
public string SavePurchaseOrder()
{
return _repository.Save();
}
}
现在让我们看看如何访问这个 `PurchaseBl` 以及如何通过构造函数传递依赖项
//Creating dependency
IRepository dbRepository = new Repository();
//Passing dependency
PurchaseBl purchaseBl = new PurchaseBl(dbRepository);
从上面的例子中,我们看到 `PurchaseBl` 依赖于 `IRepository`,并且 `PurchaseBl` 不直接创建任何仓库实例。这个仓库将由类的外部提供。因此,我们的 `PurchaseBl` 独立于低层类,它提供了松散耦合。
这里,`Repository` 类实现了 `IRepository`,它将信息保存到数据库。现在想一下,如果你需要将信息保存到 `TextFile`,那么你不需要更改 `PurchaseBL` 类。你只需要传递另一个 `TextRepository` 类,它负责将数据保存到文本文件。现在让我向你展示,如果你有 `TextRepository` 类,那么你将如何传递那个仓库。
IRepository textRepository = new TextRepository();
PurchaseBl purchaseBl = new PurchaseBl(textRepository);
因此,您可以在不影响高层类 `PurchaseBl` 的情况下更改依赖项。
Setter 注入
这是另一种 DI 技术,我们通过 setter 而不是构造函数传递依赖项。那么我们需要做什么才能实现 setter 注入呢?
在依赖类中创建 setter:在 C# 中,您只需创建一个属性。使用该属性来设置依赖项。
通过 setter 传递依赖项:在这里,我们可以通过 setter 传递依赖项。我们还可以创建不带依赖项的类对象。
让我们看看如何创建 setter 注入
public class PurchaseBl
{
//Property
public IRepository Repository { get; set; }
//Here, we are not creating any constructor which takes dependencies
public string SavePurchaseOrder()
{
return Repository.Save();
}
}
现在让我们通过 setter 注入依赖项。
//Creating dependency
IRepository dbRepository = new Repository();
PurchaseBl purchaseBl = new PurchaseBl();
//Passing dependency through setter
purchaseBl.Repository = dbRepository;
Console.WriteLine(purchaseBl.SavePurchaseOrder());
它具有一定的灵活性,我们可以在对象创建后更改依赖项,也可以创建不带依赖项的对象。setter 注入需要注意一点。由于我们可以创建不带依赖项的对象,因此在使用未设置/注入的依赖项时可能会抛出异常。
接口注入
这并不常用,与其他方法相比更为复杂。依赖类实现一个接口。接口有一个用于设置依赖项的方法签名。注入器使用该接口来设置依赖项。因此,我们可以说依赖类实现了接口,并且有一个方法来设置依赖项,而不是使用 setter 和构造函数。
让我们看一个例子。
public interface IDependentOnTextRepository
{
//Method signature which is liable to inject/set dependency
void SetDependency(IRepository repository);
}
public class PurchaseBl :IDependentOnTextRepository
{
private IRepository _repository;
public string SavePurchaseOrder()
{
return _repository.Save();
}
public void SetDependency(IRepository repository)
{
_repository = repository;
}
}
//Creating dependency
IRepository dbRepository = new Repository();
PurchaseBl purchaseBl = new PurchaseBl();
//Passing dependency
((IDependentOnTextRepository)purchaseBl).SetDependency(dbRepository);
- 首先,我们有一个带有 setter 方法的接口类。让我们创建这个接口。
- 依赖类将实现接口。因此,根据我们的示例,`PurchaseBl` 将实现 `IDependentOnTextRepository`。
- 最后,让我们看看如何使用 `PurchaseBl`。
我觉得这比其他的要复杂一点,而且不常用。
控制反转容器 (IoC Container)
什么是 IoC 容器
- 用于进行依赖注入的框架
- 提供配置依赖项的方法
- 自动解决配置的依赖项
还不清楚吗?让我换一种方式来描述。IoC 容器是一个框架,用于创建依赖项并在需要时传递它们。这意味着我们不需要像之前的示例那样手动创建依赖项。IoC 框架会根据请求自动创建对象并在需要时注入它们。所以它减少了很多麻烦。当您看到所有依赖项都自动创建时,您会很高兴。
IoC 容器的作用
- 创建依赖项/对象
- 在需要时传递/注入依赖项
- 它管理对象的生命周期
- 它还存储了开发者定义的类映射。基于此映射,您的 IoC 容器创建对象。
- 并且做了很多事情....
有许多 IoC 框架(Unity、Ninject、Castle Windsor 等)可供使用。您可以根据需要下载它们。但在使用它们之前,我想自己创建一个 IoC 框架,这样您就能理解 IoC 框架是如何工作的。让我们可视化 IoC 容器。从图中
IoC 容器知道所有类。IoC 知道哪些是依赖项,哪些是依赖方。当依赖类请求对象时,IoC 根据配置提供请求类的实例。如果依赖类请求实现 `IRepository` 的类,那么 IoC 会根据配置考虑需要提供哪些依赖项(`DBRepository` 或 `FakeRepository`)。
手动构造函数注入
在构造函数依赖注入部分,我向您展示了一些示例。在那里,我实际上进行了手动构造函数注入。请回想一下它们
//Manually Creating dependencies
IRepository dbRepository = new Repository();
//Manually Passing dependencies
PurchaseBl purchaseBl = new PurchaseBl(dbRepository);
在这里,我正在创建依赖项,创建后,我将该依赖项传递给高级类。如果我的高级类依赖于几个低级类,那么我必须创建几个依赖项并手动传递它们。那么 IoC 容器做了什么呢?答案是 IoC 容器会自动创建依赖项,而您无需手动创建它们。我将向您展示如何创建 IoC 容器以及如何使其自动化并从中心点管理它们。
构建自己的 IoC 容器
我知道有很多 IoC 容器可用,但我在这里展示如何开发我们自己的 IoC 容器,因为这有助于理解 IoC 容器的工作原理。我不会详细展示 IoC 容器的创建过程。在这里,我只创建一个简单的 IoC 容器,它可以根据配置解决依赖关系。让我们从头开始。假设我们有以下接口和实现。
///<summary>
///A repository interface used for Persisting
///Persistance media will defined by implementation
///</summary>
public interface IRepository
{
string Save();
}
我们有以下实现。`Repository` 负责将数据存储到数据库,`TextRepository` 负责将数据存储到文本文件。现在考虑 `IRepository` 的一个简单实现
///<summary>
/// Responsible for saving data to Database
///</summary>
public class Repository : IRepository
{
public string Save()
{
return "I am saving data to Database.";
}
}
///<summary>
/// Responsible for saving data to text file
///</summary>
class TextRepository : IRepository
{
public string Save()
{
return "I am saving data to TextFile.";
}
}
现在我们有一个 `PurchaseBl` 类,它依赖于 `IRepository`。`PurchaseBl` 负责应用业务逻辑,并使用 `IRepository` 来持久化其信息。
/// This is a class where we put our business logic.
/// Here, we have SavePurchaseOrder method which is responsible for storing data.
/// But this class doesn't know where to store data.
/// It is just using implementation of IRepository to storing data.
public class PurchaseBl
{
private readonly IRepository _repository;
public PurchaseBl(IRepository repository)
{
_repository = repository;
}
public string SavePurchaseOrder()
{
return _repository.Save();
}
}
我们的业务类 `PurchaseBl` 不知道数据将存储在哪里。所以它不依赖于低层类。我们可以改变持久化机制而不改变我们的业务类。
让我们先用手动依赖注入来使用 `PurchaseBl`。
public static void Main()
{
//Creating dependency
IRepository dbRepository = new Repository();
//injecting dbRepository
PurchaseBl purchaseBl = new PurchaseBl(dbRepository);
Console.WriteLine(purchaseBl.SavePurchaseOrder());
}
输出
I am saving data to Database.
现在,如果我们想在不影响业务层的情况下将数据存储到文本文件,那么我们只需对依赖创建进行一点点更改。让我们看看
public static void Main()
{
IRepository textRepository = newTextRepository();
//Injecting textRepository
PurchaseBl purchaseBl = newPurchaseBl(textRepository);
Console.WriteLine(purchaseBl.SavePurchaseOrder());
}
输出
I am saving data to TextFile.
现在我们再进一步思考,想使用 IoC 容器,那么我们的主方法会是什么样子呢?让我们看看
public static void Main()
{
Resolver resolver = new Resolver(); //Resolver is a IoC container
PurchaseBl purchaseBl = new PurchaseBl(resolver.ResolveRepository());
Console.WriteLine(purchaseBl.SavePurchaseOrder());
}
public class Resolver
{
public IRepository ResolveRepository()
{
return new TextRepository();
}
}
但是上述技术还不够好。每次我们都必须在 `Resolver` 类上创建方法,这可能会增加复杂性。现在让我们再进一步思考。我们想解决像下面这样的依赖关系
public static void Main()
{
Resolver resolver = new Resolver();//Resolver is a IoC container
PurchaseBl purchaseBl = resolver.Resolve<purchasebl>();
Console.WriteLine(purchaseBl.SavePurchaseOrder());
}
看,我们不需要创建依赖项。IoC 容器会自动创建依赖项并注入到 `PurchaseBl` 类中。现在我们的 main 方法比以前更简单了。我说对了吗?现在我们的 IoC 容器通过读取类的类型来解析依赖项。在这里,我只是向您展示了如何使用 IoC 容器以及如何仅通过传递类型来获取对象。现在让我们创建我们自己的。在这里,我将我的简单代码和描述添加进来。
/// <summary>
/// Our simple IoC container
/// </summary>
public class Resolver
{
//This is dictionary used to keep mapping between classes
private readonly Dictionary<Type, Type> _dependencyMapping = new Dictionary<Type, Type>();
/// <summary>
/// return requested object with requested type
/// </summary>
/// <typeparam name="T">Takes type which needs to be resolved</typeparam>
/// <returns></returns>
public T Resolve<T>()
{
// cast object to resolved type
return (T)Resolve(typeof(T));
}
/// <summary>
/// This method takes the type which needs to resolve and
/// returns an object based on configuration
/// </summary>
/// <param name="typeNeedToResolve">takes type which needs to resolve</param>
/// <returns>return an object based on requested type</returns>
private object Resolve(Type typeNeedToResolve)
{
Type resolvedType;
try
{
//Taking resolved/return type from dictionary
//which was configured earlier by Register method
resolvedType = _dependencyMapping[typeNeedToResolve];
}
catch
{
//If no mapping found between requested type and
//resolved type, then it will through exception
throw new Exception(string.Format("resolve failed for {0}",
typeNeedToResolve.FullName));
}
//Getting first constructor of resolved type by reflection
var firstConstructor = resolvedType.GetConstructors().First();
//Getting first constructor's parameter by reflection
var constructorParameters = firstConstructor.GetParameters();
//if no parameter found then we don't need to think about
//other resolved type from the parameter
if (!constructorParameters.Any())
return Activator.CreateInstance(resolvedType); // returning an instance of
// resolved type
//if our resolved type has constructor, then again we have to resolve that types;
//so again, we are calling our resolve method to resolve from constructor
IList<object> parameterList = constructorParameters.Select(
parameterToResolve => Resolve(parameterToResolve.ParameterType)).ToList();
//invoking parameters to constructor
return firstConstructor.Invoke(parameterList.ToArray());
}
/// <summary>
/// This method is used to store mapping between request type and return type
/// If you request for IRepository, then what implementation will be returned;
/// you can configure it from here by writing
/// Registery<IRepository, TextRepository>()
/// That means when resolver requests for IRepository, then TextRepository will be returned
/// </summary>
/// <typeparam name="TFrom">Request Type</typeparam>
/// <typeparam name="TTo">Return Type</typeparam>
public void Registery<TFrom, TTo>()
{
_dependencyMapping.Add(typeof(TFrom), typeof(TTo));
}
}
所以最后,我们的主方法如下所示
public static void Main()
{
//registering dependencies to Container
var resolver = new Resolver();
resolver.Registery<IRepository, FakeRepository>();
resolver.Registery<PurchaseBl, PurchaseBl>();
// Resolving dependencies
var purchaseBl = resolver.Resolve<PurchaseBl>();
Console.WriteLine(purchaseBl.SavePurchaseOrder());
}
使用 IoC 框架所需了解的事项
如果您认为上述代码复杂,那么您无需理解它。您只需了解两件事即可使用 IoC 容器。
- 如何配置依赖项,即如何注册组件
- 如何解决依赖项
要解析对象,您只需编写以下代码
//Resolving dependencies
var purchaseBl = resolver.Resolve<purchasebl>;();
Console.WriteLine(purchaseBl.SavePurchaseOrder());
在这里,我没有提到对象生命周期管理,因为正如我所说,我将创建一个简单的 IoC 容器,以便您了解 IoC 的工作原理。
现在我想向您展示如何在我们的项目中使用另一个 IoC 容器 Ninject。
使用 Ninject IoC 容器
在这里,我将尝试涵盖
- Ninject 简介
- 设置容器
- 使用容器
- 生命周期管理和其他特性
Ninject
Ninject 是一个较新的开源 IoC 容器。它简单且可扩展。您可以从以下位置下载 Ninject IoC 容器:http://www.ninject.org/download.html。或者您可以使用 NuGet 将 Ninject 添加到您的项目中。让我们从 NuGet 将 Ninject 添加到我们的项目中。只需转到您的项目引用并右键单击,然后管理 NuGet 包...然后搜索 Nintject。您将看到类似以下内容:
安装 Ninject 后,您将在项目引用中找到 Ninject。要使用 Ninject,您必须使用以下命名空间
using Ninject;
设置容器
Ninject 容器使用一个名为 `kernel` 的类。Ninject 自动注册所有具体类型。您无需注册它们。
注册依赖项
var kernel = new StandardKernel();
kernel.Bind<IRepository>().To<Repository>();
当请求 `IRepository` 时,将提供 Repository 实现。
使用容器
解决依赖项
//resolving dependencies
//Out from kernel
PurchaseBl purchaseBl = kernel.Get<PurchaseBl>();
Console.WriteLine(purchaseBl.SavePurchaseOrder());
它产生以下输出。
输出
I am saving data to Database.
如果我们改变绑定,那么
var kernel = new StandardKernel();
kernel.Bind<IRepository>().To<TextRepository>();
PurchaseBl purchaseBl = kernel.Get<PurchaseBl>();
Console.WriteLine(purchaseBl.SavePurchaseOrder());
它产生以下输出。
输出
I am saving data to TextFile.
当我们遇到新的模块需要更改默认绑定时,Rebind 非常有用。例如,在某些时候,我们需要更改默认绑定。我们可以使用 `Rebind` 方法,如下所示:
kernel.Rebind<IRepository>().To<Repository>();
这些是我们使用 ninject 的简单方法。现在让我们看看如何使用 ninject 管理生命周期。
使用 Ninject 进行对象生命周期管理
让我们修改我们的 `TextRepository` 类,以便我们了解对象的生命周期。以下 `TextRepository` 是我们修改后的类。
/// <summary>
/// Responsible for saving data to text file
/// </summary>
public class TextRepository : IRepository
{
private int _counter = 0;
public string Save()
{
_counter++;
return string.Format("I am saving data to TextFile {0}.", _counter);
}
}
让我像这样创建 `PurchaseBl` 的两个实例
var kernel = new StandardKernel();
kernel.Bind<IRepository>().To<TextRepository();
//resolving dependencies
//Out from kernel
var purchaseBl = kernel.Get<PurchaseBl>();
Console.WriteLine(purchaseBl.SavePurchaseOrder());
var purchaseBl2 = kernel.Get<PurchaseBl>();
Console.WriteLine(purchaseBl2.SavePurchaseOrder());
现在我们有两个 `PurchaseOrderBl` 实例,`purchaseBl` 和 `purchaseBl2`。所以输出是
输出
I am saving data to TextFile1.
I am saving data to TextFile1.
看这里 - 两个输出都显示 1,这意味着每次都创建新的实例。现在,如果我们想创建一个 `TextRepository` 的单一实例,那么我们必须将实例创建为单例,以便同一个实例通过 `purchaseBl` 和 `purchaseBl2` 共享。
让我们这样做
kernel.Bind<IRepository>().To<TextRepository>().InSingletonScope();
输出
I am saving data to TextFile1.
I am saving data to TextFile2.
现在第一个返回 1,然后第二个返回 2 - 这意味着 `TextRepository` 的单个实例被两者共享。我们可以在许多作用域中创建对象,例如事务作用域、线程作用域、单例作用域等等。这些是我们在项目中使用 ninject 的基础。稍后,我计划撰写更多关于 ninject、Unity 和 Castle Windsor 的详细信息。
结论
我们编写了大量的代码,一段时间后,我们发现很难管理和测试它们。这仅仅是因为紧密耦合。紧密耦合使我们的系统变得僵化。DIP、IoC 和 DI 帮助我们编写松散耦合的代码,并构建独立、模块化的系统。在这里,我们看到了许多实现 IoC 的方法。在所有技术中,创建反转(带构造函数注入的依赖注入)非常常见。我想现在您已经清楚 DIP、IoC、DI 和 IoC 容器了。我只是尝试在这里将所有这些概念放在一起呈现,因为它有助于我们理解与 DIP 相关的所有事物。请记住一件事 - 没有 IDP,您就无法创建独立和可插拔的系统。在这里,我无法描述更多细节,因为它会变得过于庞大和复杂。感谢阅读我的文章!!我期待您宝贵的评论、建议和批评。
一些参考资料
- 控制反转容器和依赖注入模式
- 依赖注入揭秘
- Pluralsight 培训
- 设计模式
- http://blog.architexa.com/2010/05/types-of-dependency-injection/
- 依赖反转原则和依赖注入模式
历史
- 2013年2月7日:初始版本