这篇文章将描述 DIP、 IoC、 DI 和 IoC 容器。大多数情况下,初学者开发人员会遇到 DIP、 IoC、 DI 和 IoC 容器的问题。他们混淆在一起,发现很难辨别他们之间的区别,不知道为什么他们需要使用他们。另一方面,很多人使用 DI,IoC 却不知道它能解决什么问题。关于这个话题有很多帖子、文章和博客,但是都是分散讲解。这篇文章将描述上述概念,希望通过这篇文章,能够识别 DIP、 IoC、 DI 和 IoC 容器/框架之间的区别,也知道如何以及何时使用它们。同时,希望读者在阅读完这篇文章后,能够创建自己的 IoC 容器。下面将简单描述DIP、 IoC、 DI和IoC容器。
DIP:软件架构设计中使用的原则。
IoC:用于控制流、依赖和接口的反转模式。
DI:依赖注入,用 IoC 实现依赖关系的反转。
IoC Container:依赖注入框架,它有助于映射依赖关系,管理对象创建和生存期。
目录
DIP是一种软件设计原则,IoC是一种软件设计模式。
软件设计原则: 原则为我们提供指导。原则决定什么是对什么是错。它没有告诉我们如何解决问题。它只是提供了一些指导方针,使我们能够设计好的软件,避免糟糕的设计。有些原则是 DRY(Don’tRepeatYourself),OCP(开放封闭原则),DIP(依赖倒置原则) 等。
软件设计模式: 模式是针对软件设计中给定上下文中常见问题的通用可重用解决方案。如工厂模式,装饰器模式等。
所以原则定义了好与坏。因此,我们可以说,如果我们在软件设计过程中保持原则,那么它将是良好的设计。如果我们不坚持原则,那么我们可能会陷入困境。现在我们只关注依赖反转原则(DIP) ,它是在 SOLID 原理下的D。
依赖倒置原则(DIP)
与定义高级模块所依赖的接口的低级模块不同,高级模块定义了低级模块实现的接口。
让我们尝试用移动电源或设备来描述上述原则。考虑到你有相机,电话和其他设备。这些设备使用电缆连接计算机或充电器。一个简单的插孔或端口是用来连接计算机或充电器,对不对?现在,如果有人问你是谁定义了端口或插孔,电缆还是你的设备?
你一定会回答是设备。根据设备端口的不同。所以从这个故事中,我们发现端口并不定义什么是设备,而是设备定义什么是端口或插孔。
所以软件模块是相似的。高级模块定义接口,低级模块实现接口和低级模块不定义接口,就像插孔不定义设备一样。
让我们根据 Bob Martin 的定义再次考虑一下 DIP:
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend upon details. Details should depend upon abstractions.
因此,从原理上可以说,高级模块不应该依赖于低级模块。两者都依赖于抽象。抽象不应该依赖于低级别模块。
尝试理解DIP
依赖关系没有反转(高级模块依赖于低级接口)
从图中我们发现高级模块依赖于低级模块。所以高级类需要考虑所有的接口。当新的低级类出现时,高级类又需要改变,这使得维护变得复杂,违反了开闭原则。
现在高级类定义了接口,而高级类并不直接依赖于低级类。低级类实现了由高级类定义的接口,所以当新的实现到来时,高级类不需要更改。
没有遵守DIP的复制程序
首先,忽略虚线对象。你有一个复制程序,负责从键盘输入和写入文本文件。当前的复制程序可以同时维护它们,所以不会出现问题。
经过一段时间后,你的老板要求你开发你的复制程序,以便它可以存储读取数据到数据库。那你打算怎么办。您将再次更改为复制程序,以便它可以写入数据库。这将增加程序的复杂性。
遵守DIP的复制程序
如果我们考虑上面的例子,那么我们发现复制程序是依赖于读取接口和写入接口定义的复制程序。因此,它不直接依赖于底层类,如从扫描仪读取,写到打印机等。因此,我们不需要改变复制程序时,一个新的要求将到来,我们的复制程序是独立的。
通过上面的例子应该能理解DIP的好处了,如果我们不坚持软件设计原则,那么我们的软件设计将是糟糕的设计。现在考虑一个没有维护依赖反转原则的软件设计。由于我们没有坚持 DIP,那么我们将导致下面的问题。
不遵守DIP的缺陷
系统扩展性差: 很难在不影响系统的太多其他部分的情况下更改系统的一部分。
系统可维护性: 当我们做出改变时,系统中意想不到的部分将会崩溃。
系统复用性差: 将很难在另一个应用程序中重用它,因为它不能与当前应用程序分离。诸如此类。
DIP 的优势
首先,我们可以解决上述缺陷。这意味着我们的系统将是松散耦合的、独立的、模块化的、可测试的等等。由于 DIP 是一个原则,它没有说如何解决这个问题。如果我们想知道如何解决这个问题,那么我们必须了解(Ioc)控制反转。
控制反转(IoC)
DIP 没有告诉我们如何去解决这个问题,但控制反转定义了一些方法,通过DIP来帮助我们。IoC是您可以在软件开发中实际应用的东西。IoC 有很多可用的定义。这类给出一个简单的定义,以便我们能够容易地理解。
什么是 IoC
IoC帮助我们应用DIP。在简单的 IoC 是通过切换谁控制来反转对某事物的控制。系统的特定类或其他模块将负责从外部创建对象。控制反转意味着将控制的方式从正常的方式去改变。
IoC and DIP
DIP原则是高级模块不应该依赖于低级模块,二者都应该依赖于抽象。IoC是一种提供抽象的方式,一种改变控制的方法。IoC 提供了一些实现 DIP 的方法。如果要使上层模块与下层模块相独立,则必须反转控制,使下层模块不再控制接口和对象的创建,即IoC 提供了一种控制反转的实现方法。
分解IoC
1、接口反转。
2、控制流反转: 反转控制流,这是 IoC 的基本思想。
3、创建反转: 这是开发人员最常用的方法。使用 DI 和 IoC 容器时使用它。
DIP, IoC and DI
上面的图片将三者的关系进行呈现。DI 不仅仅是依赖创建的方式,实现依赖项创建的方法有很多,只是依赖注入为其中一种方式。最上面是 DIP,它是一种设计软件的方法。它没有系统地说明如何制作独立的模块。IoC 提供了一些应用 DIP 原则的方法。IoC 没有提供具体的实现。给出了一些实现控制反转的方法。如果我们想使用绑定反转或依赖创建来反转控制,那么我们可以通过实现依赖注入(DI)来实现。
接口反转(Interface Inversion)
假设有一个阅读器应用程序。这个阅读器应用程序是从文本文件读取的。
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b53df54212554fffbb64317d002c64f1.png)
现在假设也从PDF和Word读取
创建大量的接口并不意味着我们在实现 IoC 或遵守DPI。使用太多接口的好处是什么?在这里我们没有好处,因为每次我们必须改变我们的阅读器类和阅读器类必须维护所有的接口。所以最终我们没有受益。现在我们有一个称为接口反转的方法。
通过上面的例子,我们移除了底层类中的所有接口,并使用读取类定义了单个接口。我们在这里做的只是反转接口。现在我们有很多好处。我们不再需要改变读取类,读者类是独立的。现在将上面的内容作为生产者模型来考虑。
流反转(Flow Inversion)
下面看看关于流量反转的。流反转是一种简单的反转,是IoC的基础。
Normal Flow
对于一个命令行程序,它首先询问你的名字,然后你输入你的名字,再次它会询问你的年龄,然后你会提供你的年龄。这是怎么回事?您发现命令行应用程序一个接一个地执行,这意味着基本流程是过程性的,一步一步来。
Inverted Flow
现在我们反转流。现在我们假设有这样一个界面。我们的界面由两个输入框组成,一个用于输入用户名,另一个用于输入用户年龄。如下图:
现在您可以提供您的姓名和年龄,而不需要一步一步来。如果你愿意,你可以把你的年龄放在第一位,你的名字放在最后,当您按下保存按钮,然后您的程序将保存信息。在命令行程序中,你要求首先输入姓名,然后这再输入年龄,但在UI流程中是由用户控制先后顺序的。
创建反转(Creation Inversion)
这是最常见的控制反转方法,我们将在 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 btn = ButtonFactory.CreateButton();
所以我们的按钮取决于工厂类,当一个新的样式出现时,我们不需要改变我们的 UI 代码。在这里,对象创建是从 UI 类的外部完成的。这样,我们就反转了对象的创建过程。
创建反转的方式
工厂设计模式:
如果您看过上面的示例,那么您将发现我已经从 UI 类的外部管理了对象的创建。在工厂模式中,代码类似于
Button btn = ButtonFactory.CreateButton();
服务定位模式(Service Locator)
Button btn = ServiceLocator.Create(IButtonControl);
在服务定位器模式中,我们传递接口,服务定位器将提供请求接口的相应实现。在这里,不在描述细节,专注于描述依赖注入(DI)。
Dependency Injection (DI)
在依赖注入中,我们通过依赖关系,考虑下面的简单代码
Button btn = GetButtonInstanceBasedOnUserSettings();
OurUi ourUI = new OurUi(btn);
这里,我们在创建UI时传递了依赖项。在 DI中,关键思想是传递依赖关系。DI 不仅仅是构造函数注入。在下一节中,我将描述关于 DI 的更多细节。
还有更多的方法来创建反转。所以总的来说,任何时候你把依赖关系和依赖关系从类中绑定出来的时候,你都是在反转控制,这就可以被看作是控制反转(IoC)。
依赖注入(DI)
一般地,容易混淆DI和IoC,IoC描述了反转控制的不同方法。然而,DI通过传递依赖项来反转控制,相当于依赖注入(DI)相当于是IoC的一种实现方式。
依赖注入是啥?
对于IoC,我们将依赖关系和绑定的创建移到依赖它的类之外。通常,对象是在依赖类内创建的,并且在依赖类进行关联。在DI中,它将从依赖的类的外部完成上述关系的创建与关联。
假设你带了一个早餐盒到你的办公室吃早餐。所以你把早餐需要的东西都带来了。所以你是在早餐盒管理早餐。在某种意义上,这类似于类内部的创建对象,管理所需的内容。
现在假设有人送餐到你的办公室,你不需要带早餐盒。这样你仍然可以吃早餐,但现在是由公司或者其他提供的。这类似于在类之外提供所需对象的依赖注入。
依赖注入的方式
构造函数注入(Constructor Injection)
这是最常见的依赖注入方式。通过构造函数将依赖项传递给依赖类。注入者创建依赖项并通过构造函数传递依赖项。让我们考虑下面的例子。我们有一个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 Injection)
这是另一种类型的DI技术,我们通过setter而不是构造函数传递依赖关系。那么,我们需要做些什么来实现注入呢?
在依赖类中创建属性setter: 在 C#中,您只需要创建一个属性。
通过属性传递依赖项: 在这里,我们可以通过属性传递依赖项。我们没有创建任何构造函数来传递依赖关系。
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();
}
}
通过属性进行进行依赖注入:
//Creating dependency
IRepository dbRepository = new Repository();
PurchaseBl purchaseBl = new PurchaseBl();
//Passing dependency through setter
purchaseBl.Repository = dbRepository;
Console.WriteLine(purchaseBl.SavePurchaseOrder());
这种方式具有灵活性,我们可以在创建对象之后更改依赖关系,也可以创建没有依赖关系的对象。有一个注意事项,因为我们没有直接创建依赖关系的对象,所以当使用依赖关系时而没有注入属性时,它会导致异常。
接口注入(Interface Injection)
这是最不常用的方式,它比前两者复杂。依赖类实现该接口,该接口为拥有用于设置依赖项的方法签名。注入者使用接口来设置依赖项。因此,我们可以说依赖类是该接口的一个实现,并且有一个方法来设置依赖关系,而不是使用属性和构造函数。
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);
使用接口注入的步骤为,首先,我们有一个带设置依赖方法的接口类。然后依赖类将实现该接口,根据我们的示例,PurchaseBl将实现IDependentOnTextRepository。
IoC容器(IoC Container)
IoC Container是啥
简单描述是依赖注入的框架提,供配置依赖项的方法,可以自动解析已配置的依赖项。用另一种方式来描述,IoC 容器是一个创建依赖项并在需要时传递它们的框架。这意味着我们不需要像前面的示例那样手动创建依赖项。IoC框架根据请求自动创建对象,并在需要时注入对象。
IoC容器可以干啥
创建依赖项对象,在需要时传递/注入依赖项,它管理对象生命周期,还可以存储开发人员定义的类的映射。还有其他很多功能。
有很多IoC框架(Unity、 Ninject、Castle Windsor等)都可以使用,可以根据需要下载它们。但是在使用它们之前,我们可以通过自己创建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.";
}
}
下面创建依赖于IRepository的PurchaseBl类。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 Container,那么我们的主方法将是什么样子的?
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();
}
}
但是上述技术还不够好。每一次,我们都必须在解析器类上创建方法,这可能会增加复杂性。现在让我们进一步思考。我们希望像下面这样解决依赖关系:
public static void Main()
{
Resolver resolver = new Resolver();//Resolver is a IoC container
PurchaseBl purchaseBl = resolver.Resolve<purchasebl>();
Console.WriteLine(purchaseBl.SavePurchaseOrder());
}
根据上面代码,我们不需要创建依赖项。IoC容器会自动创建依赖项并注入PurchaseBl 类。现在我们的主要方法比以前更简单了。现在我们的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 容器的两件事。
1、如何配置称为如何注册组件的依赖项;
2、如何解决依赖关系。
要解析对象,只需编写以下代码:
//Resolving dependencies
var purchaseBl = resolver.Resolve<purchasebl>;();
Console.WriteLine(purchaseBl.SavePurchaseOrder());
在这里,没有讨论任何关于对象生命周期时间管理的内容,因为正如我所说的,这里只是创建一个简单的IoC容器,帮助理解IoC是如何工作的。
下一篇文章将展示在我们的项目中如何使用一个IoC容器: Ninject。