依赖注入模式 – DI (*)
首先,“依赖注入模式”是一种软件设计模式。它被称为“模式”,因为它建议针对特定问题的低级特定实现。
该模式旨在解决的主要问题是如何创建“松散耦合”的组件。它通过将组件的创建与其依赖项分开来实现这一点。
此模式中有四个主要角色(类):
Client:客户端是一个组件/类,它想要使用另一个组件提供的服务,称为Service.
Service-Interface:服务接口是描述服务组件提供什么样的服务的抽象。
Service:Service组件/类根据服务接口描述提供服务。
Injector:是一个组件/类,其任务是创建客户端和服务组件并将它们组装在一起。
它的工作方式是 Client 依赖于 Service-Interface IService。Client 依赖于IService接口,但不依赖于 Service 本身。服务实现IService接口并提供客户需要的某些服务。Injector 创建Client和Service对象并将它们组合在一起。我们说注入器将服务“注入”到客户端。
基于注入方法的依赖注入类型
通常在文献 [1] 中,人们会发现他们提到了不同类型的依赖注入,根据将服务注入客户端的方法进行分类。我认为这不是一个重要的区别,因为效果总是相同的,即对服务的引用被传递给客户端,无论如何。但是,为了完整起见,让我们解释一下。
因此,依赖注入的类型有:
构造函数注入——注入在Client构造函数中完成
方法注入——注入是通过专用方法完成的
属性注入——注入是通过public属性完成的
Client不知道正在注入哪个服务,如果是Service1,Service2或Service3。这就是我们想要的,我们看到组件/类、、、Client和是“松散耦合的”。Service1Service2Service3
Client类现在更具可重用性和可测试性。此功能的一个典型用法是在生产环境Client中注入真实服务Service1,在测试Client中注入环境Service2,这是一个仅为测试而创建的 Mock 服务。
这种模式的好处
这种模式的好处是:
创建松散耦合的组件/类Client和Service
客户端不依赖或不了解服务,这使其更具可重用性和可测试性
允许不同开发人员/团队并行开发组件/类客户端和服务,因为它们之间的边界由IService接口明确定义
它简化了组件的单元测试
这种模式带来的缺点是:
更努力地规划、创建和维护界面
依赖注入器来组装组件/类
类似的模式
这种模式与 GoF 书籍 Strategy Pattern [2] 非常相似。类图实际上是相同的。区别在于意图:
依赖注入更像是结构模式,其目的是组装松散耦合的组件,一旦组装,它们通常在客户端生命周期内保持这种状态;尽管
策略模式是行为模式,其目的是为问题提供不同的算法,这些算法通常在客户端生命周期内可以互换。
依赖倒置原则 – DIP (**)
因此,“依赖倒置原则(DIP)”是一种软件设计原则。它被称为“原则”,因为它提供了有关如何设计软件产品的高级建议。
DIP 是 Robert C. Martin [5] 提出的以首字母缩写词 SOLID [3] 着称的五种设计原则之一。DIP 原则规定:
高级模块不应该依赖于低级模块。两者都应该依赖于抽象。
抽象不应该依赖于细节。细节应该取决于抽象。
解释是:
虽然高级原则谈到“抽象”,但我们需要将其转换为我们特定编程环境中的术语,在本例中为 C#/.NET。C# 中的抽象是通过接口和抽象类来实现的。当谈到“细节”时,原则意味着“具体实施”。
因此,基本上,这意味着 DIP 促进了 C# 中接口的使用,具体实现(低级模块)应该依赖于接口。
控制反转 – IoC (***)
同样,“控制反转 (IoC)”是软件设计原则。它被称为“原则”,因为它提供了有关如何设计软件产品的高级建议。
在传统编程中,自定义代码始终具有流控制并调用库来执行任务。
IoC 原则建议(有时)将控制流给予库(“框架”),库(“框架”)将调用自定义代码来执行任务。
当他们说“框架”时,他们的意思是为特定任务设计的专门的、任意复杂的可重用模块/库,并且自定义代码的编写方式使其可以与该“框架”一起使用。我们说“控制流是倒置的”,因为现在“框架”调用了自定义代码。
该框架在控制应用程序活动中扮演主程序的角色。程序的主要控制是倒置的,从你移到框架上。控制反转是使框架不同于库的关键部分([26])。
IoC 原则促进了实现常见场景的可重用“软件框架”的开发和使用。然后,编写特定问题的自定义代码并使其与“框架”一起工作以解决特定任务。
虽然 IoC 原则经常在紧随其后的 Dependency Injection Pattern (*) 的上下文中被提及,但它是一个更广泛的概念。例如,基于事件处理程序/回调方法的“UI 框架”也遵循 IoC 原则。更多解释参见[26]、[25]、[8]。
依赖注入模式 (*) 遵循这一原则,因为正常的传统方法是让客户端创建服务并建立依赖关系。这里控制是倒置的,即服务的创建和依赖的创建被委托给注入器,在这种情况下,注入器就是“框架”。
依赖注入容器 (****)
因此,“依赖注入容器(DI Container)”是一个软件模块/库,可以通过许多高级选项实现自动依赖注入。
在 IoC 原则 (***) 的术语中,DI Container 具有“框架”的作用,因此您经常会看到它被称为“ DI 框架”,但我认为“框架”这个词被过度使用了,它导致混乱(你有 ASP MVC 框架、DI 框架、实体框架等)。
通常在文献中,它被称为“ IoC Container ”,但我认为 IoC 原则 (***) 是比 DI 模式 (*) 更广泛的概念,这里我们将真正的 DI 模式实现放在一个大规模。因此,“DI Container”是一个更好的名称,但“IoC Container”这个名称非常流行,并且被广泛用于同一事物。
什么是DI容器
还记得 DI 模式 (*) 和 Injector 的作用吗?因此,DI Container 是一个高级模块/库,可同时充当许多服务的注入器。它可以大规模实现DI模式,具有许多高级功能。DI Containers 是一种非常流行的架构机制,许多流行的框架(例如 ASP MVC)计划并支持 DI Containers 的集成。
最受欢迎的 DI 容器是 Autofac [10]、Unity [15]、Ninject [16]、Castle Windsor [17] 等。
DI 容器功能
一个 DI Container 将提供的典型功能是:
注册映射。您需要告诉 DI 容器在抽象(接口)到具体实现(类)之间的映射,以便它可以正确注入正确的类型。在这里,您向容器提供其工作所需的基本信息。
管理对象范围和生命周期。您需要告诉容器它创建的对象将具有什么范围和生命周期。
典型的“生活方式”模式是:
Singleton:始终使用对象的单个实例。
Transient:每次创建对象的新实例时。
Scoped:这通常是每个隐式或显式定义的范围的单例模式。
例如,您需要告诉容器,您是否希望每次解析依赖项时都创建一个新对象,或者您是否希望应用单例模式。例如,单例可以是每个进程、每个线程或每个“用户定义的范围”。此外,您需要指定对象的所需生命周期。例如,您可以将每个进程的对象生命周期或对象生命周期配置为每个“用户定义的范围”,这意味着对象将在用户定义的范围结束时被释放。容器可以强制执行所有这些,只需要精确配置。
解决方法。这是创建和组装所需对象/类型的实际工作。容器创建特定类型的对象,解析所有依赖项,并将它们注入到创建的对象中。该方法以递归方式深入工作,直到解决所有依赖项。DI Container 通过使用反射和类似技术来解决依赖关系。