天天看点

深入理解控制反转和依赖注入

之前一直都不太理解这两个概念,这段时间在学习ASP.NET CORE,而ASP.NET CORE是建立在依赖注入框架之上的,所以认真学习了一下。

所谓控制反转,其实就是从主动变成被动。以前我们在应用程序中要new一个对象就是很直接地new,这样做其实是违反了程序设计中最重要的一个原则:低耦合。

因为在A类中直接引用B类就造成A是直接依赖B的,这样设计出来的程序可扩展性是很差的,这种方式我们称之为主动。

那什么是被动呢?被动就是A类如果需要引用B类,不是在A类中直接实例化B类,而是由框架来提供B类的实例。

这种情况下,一般B类是继承自某个接口或者抽象类的,A类引用的是接口或者抽象类,这样就符合面向接口编程的设计原则了,A和B就解耦了。

那说到这里,大家肯定会问,那框架怎么提供B类的实例呢?

其实有很多种方式,比如:模板方法、工厂方法、抽象工厂。这些方法的理念其实都差不多:定义一个接口或抽象类C,让B继承C,在B中实现

那些抽象的方法或者虚方法,这个就不详细介绍了。

依赖注入其实是实现控制反转的一种框架,控制反转是一种设计理念,而依赖注入是实现这个理念的一种框架,这也是为什么他们兄弟俩经常被

一起提起的原因。

依赖注入通俗一点的解释是:假如A依赖B,B继承自接口或抽象类C,那A类中引用的应该是C,使A与B解耦。那注入怎么解释呢?其实就是在编写代码时

配置C的具体实现类,程序启动后,依赖注入框架会把C和B的关系存储在一个ServiceDescriptor类中,当A需要使用C时,依赖注入框架会根据B和C的映射关系实例化一个B给到A。

这样看来,依赖注入这个名字起的还是挺贴切是吧。

那依赖注入框架具体是怎么提供B类的实例呢?下面我们从纯理论的角度来说明一下依赖注入框架的内部实现(最好是下载ASP.NET CORE的源码来看,这样会了解的更加详细)

依赖注入的方式有3中:构造器注入、属性注入、方法注入。我们最常用的是构造器注入,当然在ASP.NET CORE的startup类的ConfigureServices和Configure函数中也使用了方法注入。

我们先讲一下依赖注入的注册过程吧。大家都知道我们通过调用IServiceCollection的Add方法来注册服务(IServiceCollection有几个扩展方法,可以添加不同生命周期的服务,但最终都是调用Add方法),

那这个框架具体是怎么存储这些注册信息的呢?如果不考虑细节的话,其实也挺简单的。就是在ServiceCollection(IServiceCollection接口的默认实现)中定义了List<ServiceDescriptor>类型的全局变量,

一个注册的服务的所有参数都定义在ServiceDescriptor类中,比如:服务类型(ServiceType)、实现类型(ImplementationType)、生命周期(Lifetime)等属性。每添加一个服务注册,这个List就添加一个ServiceDescriptor。

注册说完了,我们再说一下服务的消费,就是框架如何实例化对应的实例。

我们先来说一下注册的实现类需要满足的一些条件吧。第一这个实现类必须要有一个公共的构造函数,不然怎么实例化呢?第二构造函数如果有参数的话,那参数必须是

依赖注入框架必须能够提供的,比如这个参数是已经注册到框架的某个接口或者抽象类。

这里引申出了2个问题,下面我们看一下代码:

services.AddSingleton<IFoo, Foo>();
services.AddSingleton<IBar, Bar>();
services.AddSingleton<IQux, Qux>();
           
public class Qux:IQux
    {
        public Qux(IFoo foo)
        {
            Console.WriteLine($"Selected constructor:Qux(IFoo foo)");
        }
        public Qux(IFoo foo,IBar bar)
        {
            Console.WriteLine($"Selected constructor:Qux(IFoo foo,IBar bar)");
        }
    }
           

Qux有2个构造函数,那依赖注入框架会选择哪一个呢?我们在把IQux放到某个控制器的构造函数中,运行起来看一下:

深入理解控制反转和依赖注入

被选择构造函数是第二个,这个例子说明构造函数的参数必须是依赖注入框架能够提供的所有参数的子集。如果找不到匹配的参数就会报错。

我们再看一种情况:

public class Qux:IQux
    {
        public Qux(IFoo foo,IBar bar)
        {
            Console.WriteLine($"Selected constructor:Qux(IFoo foo,IBar bar)");
        }
        public Qux(IFoo foo,IBaz baz)
        {
            Console.WriteLine($"Selected constructor:Qux(IFoo foo,IBaz baz)");
        }
    }
           
services.AddSingleton<IFoo, Foo>();
services.AddSingleton<IBar, Bar>();
services.AddSingleton<IBaz, Baz>();
services.AddSingleton<IQux, Qux>();
           

这种情况下这2个构造函数都满足,框架就无法选择合适的构造函数,直接报错了。

结合生命周期,我们再考虑深入一点,如果Qux的生命周期为Singleton,Foo的生命周期为Scope,而Qux引用了Foo,那会发生什么呢?

services.AddScoped<IFoo, Foo>();          
services.AddSingleton<IQux, Qux>();
           
public class Qux:IQux
    {
        public Qux(IFoo foo)
        {
            Console.WriteLine($"Selected constructor:Qux(IFoo foo)");
        }
 
    }
           

运行结果为:

System.AggregateException:“Error while validating the service descriptor 'ServiceType: ValidateScope.IQux Lifetime: Singleton ImplementationType: ValidateScope.Qux': Cannot consume scoped service 'Vali”
           

意思就是生命周期为Singleton的类型Qux不能消费生命周期为Scope的Foo。这个其实也很好理解,Singleton服务是存储在根容器的,是单例的,它的生命周期是从程序启动开始到程序停止运行结束的,贯穿整个应用程序的生命周期。

而Scope服务是存储在根容器下的子容器的,它的生命周期是从一个请求开始到结束的,如果Singleton服务引用了Scope服务,就相当于把Scope服务放到Singleton服务里面去了,但是注册的时候是注册为Scope服务,所以这个肯定是

冲突的。

下面说一下服务实例的创建过程,一共有4中方式创建服务的实例:

1、RuntimeServiceProviderEngine,采用反射的方式提供服务实例

2、ILEmitServiceProviderEngine,采用IL Emit的方式提供服务实例

3、ExpressionsServiceProviderEngine,采用表达式树的方式提供服务实例

4、DynamicServiceProviderEngine,根据请求并发数量动态决定最终的服务实例提供方案

框架默认采用的方式是第四种。其实我也只懂反射的方式,其他的也没怎么了解,大家有兴趣的话可以自己下载ASP.NET CORE的源码研究一下。

以上就是我对控制反转和依赖注入的理解,如有说的不对的地方,请留言指正,谢谢!

继续阅读