依赖注入 (DI) 是.NET中一个非常重要的软件设计模式,它可以帮助我们更好地管理和组织组件,提高代码的可读性
Posted mabenlei
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了依赖注入 (DI) 是.NET中一个非常重要的软件设计模式,它可以帮助我们更好地管理和组织组件,提高代码的可读性相关的知识,希望对你有一定的参考价值。
依赖注入 (DI) 是.NET中一个非常重要的软件设计模式,它可以帮助我们更好地管理和组织组件,提高代码的可读性,扩展性和可测试性。在日常工作中,我们一定遇见过这些问题或者疑惑。
- Singleton服务为什么不能依赖Scoped服务?
- 多个构造函数的选择机制?
- 源码是如何识别循环依赖的?
虽然我们可能已经知道了答案,但本文将通过阅读CLR源码的方式来学习DI实现机制,同时也更加深入地理解上述问题。如果您不想阅读源码,可以直接跳至文末的解决方案。
一、源码解读
理论知识
理论篇可以先看一下,防止在下文代码不知道这些对象的作用。如果有些概念不是很清晰可以先记着,带入下文源码,应该就可以理解
ServiceProvider: ServiceProvider(依赖注入容器)不仅对外提供GetService()、GetRequiredService()方法,还可以方便地注册和管理应用程序需要的各种服务。
通过创建ServiceProvider的方式,我们可以更好地理解管理和控制服务实例的生命周期和依赖关系。
-
应用程序级别的根级ServiceProvider
.NET Core应用程序通常会使用一个应用程序级别的根级ServiceProvider,它是全局唯一的,并且负责维护所有单例服务的实例。这个实例通常是由WebHostBuilder、HostBuilder或ServiceCollection等类创建和配置的,可以通过IServiceProvider接口来访问。 -
每个请求的作用域级别的ServiceProvider
除了根级ServiceProvider之外,在.NET Core中还可以创建每个请求的作用域级别的ServiceProvider,它通常用于管理Scoped和Transient服务的生命周期和依赖关系。每个作用域级别的ServiceProvider都有自己独立的作用域,可以通过IServiceScopeFactory创建,同时也继承了根级ServiceProvider中注册的所有单例服务的实例。 -
自定义级别的ServiceProvider
在某些情况下,我们可能需要自定义级别的ServiceProvider来满足特定的业务需求,例如,将多个ServiceProvider组合起来以提供更高级别的服务解析和管理功能。此时,我们可以通过实现IServiceProviderFactory接口和IServiceProviderBuilder接口来创建和配置自定义级别的ServiceProvider,从而实现更灵活、可扩展的依赖注入框架。
生命周期管理: 我们可以将依赖注入容器看作一个树形结构,其中root节点的子节点是Scoped节点,每个Scoped节点的子节点是Transient节点(如果存在)。在容器初始化时,会在root节点下创建和缓存所有单例服务的实例,以及创建第一个Scoped节点。每个Scoped节点下都有一个独立的作用域,用于管理Scoped服务的生命周期和依赖关系,同时还继承了父级节点(即root或其他Scoped节点)的所有单例服务的实例。
在处理每个新的请求时,依赖注入容器会创建一个新的Scoped节点,并在该节点下创建和缓存该请求所需的所有Scoped服务的实例。在完成请求处理后,该Scoped节点及其下属的服务实例也将被销毁,从而确保Scoped服务实例的生命周期与请求的作用域相对应。
重要对象
-
IServiceCollection: 用于注册应用程序所需的服务实例,并将其添加到依赖注入容器中。
-
IServiceScopeFactory: 用于创建依赖注入作用域(IServiceScope)的工厂类。每个IServiceScope都可以独立地管理Scoped和Transient类型的服务实例,并在作用域结束时释放所有资源。IServiceScope通过ServiceProvider属性来访问该作用域内的服务实例
-
ServiceProvider: 可以看作是一个服务容器,它可以方便地注册、提供和管理应用程序需要的各种服务。还支持创建依赖注入作用域(IServiceScope),可以更好地管理和控制服务实例的生命周期和依赖关系
-
IServiceProviderFactory: 创建最终的依赖注入容器(IServiceProvider),提供默认的DefaultServiceProviderFactory(也就是官方自带的IOC),也支持自定义的,比如autofac的AutofacServiceProviderFactory工厂。
-
ServiceProviderEngineScope: 实现了IServiceProvider和IDisposable接口,用于创建和管理依赖注入作用域(Scope)。通过使用ServiceProviderEngineScope,我们可以访问依赖注入作用域中的服务实例,并实现Scoped和Transient类型的服务实例的生命周期管理。作用域机制可以帮助我们更好地管理和控制应用程序的各个组件之间的依赖关系
-
CallSiteFactory: 通常由依赖注入容器(如ServiceProvider)在服务解析过程中使用。当容器需要解析某个服务时,它会创建一个CallSiteFactory对象,并使用其中的静态方法来创建对应的ServiceCallSite对象。然后,容器会将这些ServiceCallSite对象组合成一个树形结构,最终构建出整个服务实例的解析树。
-
ServiceCallSite: 表示服务的解析过程。它包含了服务类型、服务的生命周期、以及从容器中获取服务实例的方法等信息
-
CallSiteVisitor: 通常由依赖注入容器(如ServiceProvider)在服务解析过程中使用。当容器需要解析某个服务时,它会创建一个ServiceCallSite的对象图,并将其传递给CallSiteVisitor进行遍历和访问。CallSiteVisitor通过调用不同节点的虚拟方法,将每个节点的信息收集起来,并最终构建出服务实例的解析树。
-
CallSiteValidator 通常由依赖注入容器(如ServiceProvider)在服务解析过程中使用,用于验证ServiceCallSite对象图的正确性。它提供了一组检查方法,可以检测ServiceCallSite对象图中可能存在的循环依赖、未注册的服务类型和生命周期问题等
.NET Core Web API使用依赖注入(DI)进行服务配置一
ASP.NET Core的框架的设计从头到尾都是模块化的,并且遵循良好的软件工程实践。在面向对象的程序设计中,SOLID经受住了时间的考验。ASP.NET Core已经把依赖注入融入了核心框架。不管你自己是否想把依赖注入运用到自己的代码中,ASP.NET Core框架已经把依赖注入当成了一个基本概念。
SOLID是 Single responsibility, Open-closed, Liskov substitution, Interface segregation, Dependeny inversion的简写:https://zh.wikipedia.org/zh-hans/SOLID_(面向对象设计)
依赖注入的介绍参考链接:https://www.martinfowler.com/articles/injection.html
1.明白依赖注入的好处
在简单应用程序中我们使用不到依赖注入,但当应用程序变得复杂得时候,依赖注入(DI)可以作为一个伟大的工具来控制代码的复杂性。
让我们看下面一个例子,当不使用DI时,一个用户在你的Web应用程序上注册后,你想给它发送一封邮件,下面的代码展示了你可能会在最初的时候这样处理问题。
在这个例子中,当一个新用户注册你的Web应用程序时,在UserControllor中RegeditUser
将执行。首先创建一个EmailSender类的实例,并且调用SendEmail()方法去发送邮件。在这个例子中,你可以设想将看到以下的内容:
Console.WriteLine将会执行发送email。
如果EmailSender类像这个例子一样简单并且没有依赖项,你可能不会考虑使用其它方法来创建对象。在某种程度上,你可能时对的。但是如果你稍后更新EmailSender的实现逻辑,发现它并不能实现自身邮件发送的全部逻辑。
实际上,EmailSender在发送邮件时需要做大量的事情:
1.创建一个email消息
2.配置email服务的设置
3.将邮件发送给emial服务器
如果在一个类中实现以上功能,将会违背单位职责原则(SRP),所以不能让EmialSender依赖其它服务。下图将会展示Web应用程序的依赖将会是怎样的。UserController想要使用EmailSender去发送邮件,但是如果要这样做,首先需要创建一个MessageFactory,NetworkClient,EmailServerSettings对象给EmailSender提供依赖。
每一个类都需要大量的依赖,在这个例子中UserController作为“底部”的类,需要知道如何创建它所依赖的每一个类,以及每一个类的依赖关系。
多依赖服务的例子如下:
NetworkClient依赖的实例代码如下:
用这样的方式可能觉得很麻烦,但这样的依赖链却是很常见的。实际上,你的代码中并没有这样写,那你的类将会很庞大,并且违背了单一职责原则。当你手动创建依赖时,发送邮件时是没有DI的。代码如下所示:
上述代码示例变成了一些粗糙的代码。当在UserController调用时,为提高EmailSender的设计性,以分离出不同的职责,这成为了一个麻烦事。上述代码有以下问题。
1.没有遵循单一职责原则-我们的代码同时负责创建EmailSender实例并且使用它发送邮件。
2.相当大的方法代码-上面的RegeditUser方法,只有最后两行代码才有用。很难读懂这个方法的真正目的是什么。
3.与实现绑定-如果你决定去重构EmialSender并且去添加另一个依赖,你需要去更新它每一个使用的地方。
UserController已经明确的依赖了EmailSender类,并且手动创建了这个对象的实例作为RegeditUser方法的一部门。在阅读源代码时仅仅知道UserController使用了EmailSender。相比之下,EmialSender已经明确依赖了NetworkClient和MessageFactory,并且在构造函数中进行了初始化。同样的,NetworkClient已经明确的依赖了EmailServerSetting类。
依赖注入旨在决绝反转依赖链。以代替UserController去手动的创建依赖,深入代码实现的细节中,已经创建了通过构造函数注入EmailSender创建的实例。
现在,很明显了,需要一些事情去创建对象,所以这些代码对象需要在某处存活。负责创建对象的服务称为 DI 容器或IoC容器。常见的DI容器有:Autofac,StructureMap, Unity, Ninject, Simple Injector . . .
使用DI容器的关系图如下:
此时UserController中的代码如下图所示:
DI容器的优势就说职责单一:创建对象或服务。向容器请求一个服务的实例,并且计算出增氧创建一个依赖图,但这些都依赖于怎样配置它们。
使用明显的依赖的优势就是,在一个方法中你永远不会在写混乱的方法了。DI容器可以检查你的服务的构造方法并且计算出代码自身中重要的部分。DI容器是可以配置的,所以你可以手动创建可提供的服务的实例。它可以通过接口让你的代码保持松耦合。
在面向对象语言中耦合是一个很重要的概念。它指出一个类怎样去依赖其它类并且执行它自己的函数。松耦合的代码不需要知道一个特别组件的很多细节,就直接可以去使用它。
在UserController中初始化EmailSender就说紧耦合的,你需要直接的创建EmialSender的实例。一个难点就是这个代码很难去测试,任何尝试测试UserController的方法将导致发送邮件。将 EmailSender 作为构造函数参数,并删除创建对象的职责将会减少代码的耦合。如果EmialSender的实现变了,让它依赖于另一个实现,不需要在同时更新更新UserController。
通过接口变成在设计模式中是一种常见的方法,它减少了系统中的耦合。并且还不会绑定到单一的实现,并且这也让类变的可以测试。所以可以创建一个IEmialSender接口,让EmailSender实现它:
用户控制器的代码如下:
现在UserController已经不用关心依赖是怎么实现的了,仅仅实现了IEmailSender接口并且暴露了SendEmail方法。现在应用程序的代码已经独立于实现了。现在已经实现了松耦合的代码了。但是又有一个问题:应用程序怎么知道使用EmailSender去代替DummyEmailSender,DI容器告诉你答案:当你需要IEmailSender,使用EmailSender去注册。
怎样在DI容器中注册接口和实现非常依赖DI容器的实现方式,但是基本所有DI容器的原则都是一样的。ASP.NET Core包含开箱即用的简单DI容器,所以让我们看下在一个典型的请求中怎样使用它。
2.在ASP.NET Core中使用依赖注入
本章详细的内容将在下篇博客中详细说明。
以上是关于依赖注入 (DI) 是.NET中一个非常重要的软件设计模式,它可以帮助我们更好地管理和组织组件,提高代码的可读性的主要内容,如果未能解决你的问题,请参考以下文章