ASP.NET Core(二):IOC、DI,即依赖注入和控制反转

发布时间 2023-05-20 11:55:58作者: AI大胜

此文只是从中摘录整理下自己感兴趣的部分,以便备忘和方便查找回顾,详见:


整个 ASP.NET Core 框架建立在一个底层的依赖注入框架之上,它使用依赖注入容器提供所需的服务对象。

服务

服务通常由组件提供,组件可以理解成对一些特定功能的实现,为了便于定制,这些组件通常以接口的形式进行标准化。

依赖注入(DI)和控制反转(IoC)

IOC: 它是框架中的一种手段或思想,让流程可复用,可定制。这里的流程是指,按照一定的顺序或逻辑调用已有的一些 API,来完成目标任务。

DI: 它用来对依赖的类型进行初始化,这是一种在类及其依赖关系之间实现控制反转 (IoC) 的技术。

为什么要使用DI,我直接对依赖的类型new一个对象不是也很方便?

参考:https://docs.microsoft.com/zh-cn/dotnet/core/extensions/dependency-injection

控制反转(IoC)介绍

控制反转(IoC)的全名 Inverse of Control,它体现的意思是控制权的转移,即控制权原来在A手中,现在需要B来接管,IoC可视为一种程序设计原则。

先了解IoC的C(Control)究竟指的是怎样一种控制。任何一项任务,不论其大小,基本上都可以分解成相应的步骤,所以任何一项任务的实施都有其固有的流程,而IoC涉及的控制可以理解为针对流程的控制

IoC的本质:IoC是设计框架所采用的一种基本思想,所谓的控制反转就是将应用对流程的控制转移到框架之中(框架提供一个引擎,应用中只需初始化这个引擎并直接启动它即可完成目标任务的整个流程)。

类库(Library)和框架(Framework)的不同之处在于:前者往往只是提供实现某种单一功能的API,而后者则针对一个目标任务对这些单一功能进行编排,从而形成一个完整的流程,并利用一个引擎来驱动这个流程自动执行。

以熟悉的ASP.NET MVC应用开发来说,我们只需要按照约定的规则(比如约定的目录结构和文件与类型命名方式等)定义相应的Controller类型和View文件就可以了。当ASP.NET MVC框架在处理请求的过程中,它会根据路由解析生成参数得到目标Controller的类型,然后自动创建Controller对象并执行它。如果目标Action方法需要呈现一个View,框架会根据预定义的目录约定找到对应的View文件(.cshtml文件),并对它实施动态编译生成对应的类型。当目标View对象创建出来之后,它执行之后生成的HTML会作为响应回复给客户端。可以看出,整个请求流程处处体现了框架Call应用这一法则。

IoC将对流程的控制从应用程序转移到框架之中,框架利用一个引擎驱动整个流程的自动化执行。应用程序无需关心工作流程的细节,它只需要启动这个引擎即可。这个引擎一旦被启动,框架就会完全按照预先编排好的流程进行工作,如果应用程序希望整个流程按照自己希望的方式被执行,需要在启动之前对流程(某一处理环节或步骤)进行定制,比如泛化的流程A、B、C,在应用程序B中可以定制成A、B1、C,即重用AC定制B1。一般来说,框架会以相应的形式提供一系列的扩展点,应用程序通过注册扩展的方式实现对流程某个环节的定制。在引擎被启动之前,应用程序将所需的扩展注册到框架之中。一旦引擎被正常启动,这些注册的扩展会自动参与到整个流程的执行过程中。

总的来说,我们在一个框架的基础上进行应用开发,就相当于在一条调试好的流水线上生产某种商品。我们只需要在相应的环节准备对应的原材料,最终下线的就是我们希望得到的产品。IoC几乎是所有框架均具有的一个固有属性。

总结一下:IoC一方面通过流程控制从应用程序向框架的反转实现了针对流程自身的重用,另一方面通过内置的扩展机制使这个被重用的流程能够自由地被定制,这两个因素决定了框架自身的价值。重用让框架不仅仅是为应用程序提供实现单一功能的API,而是提供一整套可执行的解决方案,可定制则使我们可以为不同的应用程序对框架进行定制,这无疑让框架可以使用到更多的应用之中。

另外,IoC不是一种设计模式,而是一种设计思想或原则,一般来讲,设计模式提供了一种解决某种具体问题的方案,很多设计模式都采用了IoC原则的说法才是正确的。

与IoC紧密相连的几种设计模式

依赖注入是一种对象提供型的设计模式,在这里我们将提供的对象统称为“服务”、“服务对象”或者“服务实例”。在一个采用依赖注入的应用中,我们定义某个类型的时候,只需要直接将它依赖的服务采用相应的方式注入进来就可以了。

注意如果对属性或者方法使用[Injection]这一特性,那么依赖注入容器在调用构造函数创建一个目标对象之后,它会自动为具有依赖的目标属性进行赋值。

ASP.NET Core框架使用的依赖注入框架只支持构造器注入,而不支持属性和方法注入(类似于Startup和中间件基于约定的方法注入除外),但是我们很有可能不知不觉地会按照Service Locator模式来编写我们的代码。从某种意义上讲,当我们在程序中使用IServiceProvider(表示依赖注入容器)来提取某个服务实例的时候,就意味着我们已经在使用Service Locator模式了,所以当我们遇到这种情况下的时候应该多想一想是否一定需要这么做。


接下来,介绍采用IoC原则的几种典型设计模式。

模板方法

模板方法模式与IoC的意图可以说不谋而合,该模式主张将一个可复用的工作流程或者由多个步骤组成的算法定义成模板方法, 组成这个流程或者算法的单一步骤则实现在相应的虚方法之中,模板方法根据预先编排的流程去调用这些虚方法。这些方法均定义在一个类中,我们可以通过派生该类并重写相应的虚方法的方式达到对流程定制的目的。

目的:对一个可复用的工作流程或由多个步骤组成的算法进行封装,以便满足对流程的复用和定制。

形式:定义一个类,类中定义若干虚方法(可在子类中重写),再定义一个模板方法(内部按一定的顺序调用那些虚方法)以便完成工作流程或多个步骤组成的算法。

使用:直接调用模板方法完成默认配置的流程,或者定义个子类重写某个虚方法即定制某个步骤,再调用子类的模板方法(继承而来)即可完成目标步骤或算法。

工厂方法

工厂方法,说白了就是在某个类中定义用来提供所需服务对象的方法,这个方法可以是一个单纯的虚方法,也可以是具有默认实现的虚方法。至于方法声明的返回类型,可以是一个接口或者抽象类,也可以是未封闭(Sealed)的具体类型。作为它的派生类型,可以实现或者重写工厂方法以提供所需的服务对象。

目的:在某个类中定义用来提供所需服务对象的方法,即用于对外提供对象实例。

与IoC结合使用:定义工厂方法的类中定义一个类似模板方法的东西,该模板方法内部调用那些工厂方法,获取一个个步骤所需的服务对象,并按顺序执行

抽象工厂

我们需要定义一个独立的工厂接口或者抽象工厂类,并在其中定义多个工厂方法来提供“同一系列”的多个相关对象。如果希望抽象工厂具有一组默认的“产出”,我们也可以将一个未被封闭类型作为抽象工厂,以虚方法形式定义的工厂方法将默认的对象作为返回值。在具体的应用开发中,我们可以通过实现工厂接口或者继承抽象工厂类(不一定是抽象类)的方式来定义具体工厂类,并利用它来提供一组定制的对象系列。

目的:产生一组服务对象,工厂方法更像是产生单一的服务对象

与工厂方法的区别:工厂方法用来产生单一对象,抽象工厂用来产生一组(多个)相关对象。

依赖注入(DI)

依赖和依赖注入(DI)

从面向对象编程的角度来讲,类型中的字段或者属性是依赖的一种主要体现形式。如果类型A中具有一个类型B的字段或属性,或者方法中用到另一个类型B的方法或属性,那么类型A就对类型B产生了依赖,所以可以将依赖注入简单的理解为一种针对依赖字段或属性的自动化初始化方式。实现依赖注入的方式一般分为三种:构造方法注入、属性注入、方法注入。

依赖注入是一种对象提供型的设计模式。可以将提供的对象统称为:服务、服务对象、服务实例。服务实例的提供应完全交给框架来完成,框架利用一个独立的容器(Container)来提供所需的每个服务实例。我们将这个被框架用来提供服务的容器成为依赖注入容器。依赖注入容器之所以能够按照我们希望的方式来提供所需的服务是因为该容器是根据服务注册信息创建的,服务注册包含提供所需服务实例的所有信息。

服务消费者只需要告诉容器所需服务的类型(接口、抽象类或具体类型),就能根据预先注册的规则得到与之匹配的服务实例。

即DI的使用就是换一种方式来获取目标对象(避免在需要该对象的时候直接new一个),被广泛用于使服务可被应用程序任何位置的代码使用。

依赖注入容器

机器猫的那个四次元口袋就是一个理想的依赖注入容器,大熊只需要告诉哆啦A梦相应的需求,它就能从这个口袋中得到相应的法宝。依赖注入容器亦是如此,服务消费者只需要告诉容器所需服务的类型(一般是一个服务接口或者抽象服务类),就能得到与之匹配的服务实例。我们将这个被框架用来提供服务的容器称为“依赖注入容器”。注意:依赖注入容器的使用者是框架而非应用。

简单说,DI 就是将对象的创建和销毁工作交给DI容器来进行,调用方只需要接收注入的对象实例即可。

依赖注入与Service Locator

Service Locator 是指在编程中直接获取依赖注入容器对象,进而利用该对象获取一些别的服务对象。

区别:前者的使用者是框架,后者的使用者是应用程序。另一个角度看,采用依赖注入模式的应用可以看做将服务推送到依赖注入容器,Service Locator模式下的应用则是利用Service Locator拉取所需的服务。

ASP.NET Core 和 DI

可以先参考下这篇文章:https://www.cnblogs.com/xiaoxiaotank/p/15231883.html

ASP.NET Core 框架本身带有自己的一套DI系统,且有一些默认配置好的抽象类和相应实例的映射关系。

有两种方式可以注册其他类型,

一是在StartUp类中的ConfigureServices方法中实现,即静态映射:

public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            services.AddTransient<IWeather, Weather>();
        }

使用DI系统定义的Addxxx扩展方法来绑定类型。该方法是在IServiceCollection接口上定义的。对于配置抽象类型和其对应的实现类,还有另外几个Addxxx方法。

上述这种方式很好满足了,请求一个抽象类的实例,DI系统就能返上述方法中配置的相关抽象类的实例。但是如果需要根据运行时条件将类型T解析为不同的类型,这时就得用动态解析了,动态解析允许指定一个回调函数来解析依赖。

如果为同一个抽象类型注册了多个具体类型,那么DI容器将返回最后注册的类型的一个实例。如果由于二义性或者参数不兼容,导致无法解析构造函数,那么DI容器将抛出一个异常。

在 ASP.NET Core 依赖注入框架中,我们添加的服务注册保存在 通过 IServiceCollection 接口表示的集合之中,由这个集合创建的依赖注入容器体现为一个 IServiceProvider 对象。

IServiceCollection

  • BuildServiceProvider 扩展方法,用于在添加完相关服务注册后创建出代表依赖注入容器的 IServiceProvider 对象。

IServiceProvider

  • GetService<T> 扩展方法,用于获取服务实例。

  • CreateScope 方法,用于创建代表 Scoped 生命周期的 IServiceScope 对象,该对象的 ServiceProvider 属性就是相当于一个子依赖注入容器。

    对于一个ASP.NET Core应用来说,它具有一个与当前应用绑定代表全局根容器的ISerivceProvider对象。对于处理的每一次请求,ASP.NET Core框架都会利用这个根容器来创建基于当前请求的服务范围( IServiceScope 对象),并利用后者提供的 IserviceProvider 对象来提供请求处理所需的服务实例。请求处理完成之后,创建的服务范围被终结,对应的 IServiceProvider 对象也随之被释放,此时由该 IServiceProvider 对象提供的Scoped服务实例以及实现了IDisposable接口的Transient服务实例得以及时释放 。

ASP.NET Core 中的依赖注入的服务生存期

Transient
瞬时,即每次获取,都是一个全新的服务实例。这种生存期适合轻量级、 无状态的服务。 通过 AddTransient 方法注册暂时性服务。

Scoped
范围(或称为作用域),即在某个范围(或作用域内)内,获取的始终是同一个服务实例,而不同范围(或作用域)间获取的是不同的服务实例。对于Web应用,每个请求为一个范围(或作用域),即与http请求上下文绑定在一起。通过 AddScoped 方法注册范围内服务。

Singleton
单例,即在整个应用中,获取的始终是同一个服务实例。另外,为了保证程序正常运行,要求单例服务必须是线程安全的。通过 AddSingleton 方法注册单一实例服务。 单一实例服务必须是线程安全的,并且通常在无状态服务中使用

在处理请求的应用中,当应用关闭并释放 ServiceProvider 时,会释放单一实例服务。 由于应用关闭之前不释放内存,因此请考虑单一实例服务的内存使用。

如果Singleton 服务依赖另一个Scoped服务,即Scoped 服务实例被一个 Singleton服务实例引用,这也就意味着 Scoped 服务实例成了一个Singleton服务实例。

服务注册方法

框架提供了适用于特定场景的服务注册扩展方法:

image-20210717135237151


更新于:2023.5.20