ASP.NET Core依赖注入系统学习教程:2.依赖注入的理论概念
1.依赖
在理解依赖注入之前,必须先理解其中的依赖是什么。对于我们开发的程序而言,实际上就是通过不同类型的对象相互协作而构建成的应用,例如在订单类中,就会引用商品类作为某个属性。由于类于类之间存在这种引用关系,在类中就避免不了通过“new”对引用的外部类型进行实例化,对于这种现象就会促使应用程序代码中产生依赖。
对于应用程序代码中存在的这种“依赖关系”,其实通过一种“机械齿轮图”就可以很直观的体会到这种依赖关系所带来的弊端。每个类就像某个齿轮,齿轮之间的啮合传动就像类于类之间的依赖关系。通过“机械齿轮图”的运作场景,我们不难看出这种结构的一种弊端:就是不同的齿轮会相互影响,如果有一个齿轮出了问题,就可能会影响到整个齿轮组的正常运转,对于这种现象,放到应用程序中也是无法避免的现实。
当然,这种依赖对于应用程序带来的后果并不是很直观,通常在.NET Framework时代可能很多项目都存在这种依赖现象,但这种现象并不是一个良好的设计规范,并且会带来下面的问题:
- 如果依赖的类型需要发生替换,那么所有引用该类型的类中都必须进行修改,如果引用的位置很多,覆盖点广,也就可以印证我们平时所说的“牵一发而动全身”,这种依赖是不利于程序应对需求变化的。
- 如果依赖的类型本身也依赖其他的类型,就不得为使用某个类型承担更多开销。例如A依赖B,B又依赖C、D、E,以此类推可能存在更多的嵌套依赖关系。对于这种情况,A类为了使用B类,则不得不承担创建B类的依赖项。那么如果这个A类要发生替换,并且在很多地方被引用,这个修改量会是几何倍的增长。
- 依赖项只有一种固定的实现方式,无法Mock很不易于进行单元测试。
依赖注入(DI)最终的目的就是解除上面我们所说的依赖,从而实现松耦合的软件架构体系,并且它延用了控制反转的思想,扩展为依赖关系的反转,即将“依赖关系”(具体可以说是创建依赖对象)这件事,从应用程序中转移到框架之中,这样一来应用程序就不必通过硬编码的形式创建对象,而是由框架提供,从而降低与引用的类型的依赖程度。
依赖注入本身和控制反转一样,都并不属于某个编程语言的特定领域,而是属于一种软件设计模式,在不同的开发领域有不同的体现形式,例如.NET Core中内置的依赖注入框架、还有Java的Spring、第三方的PicoContainer等等框架。
2.粘合剂
在我们应用程序代码中通过硬编码“new”的方式是一种产生依赖的对象创建方式,为了解除这种依赖方式,依赖注入框架为我们提供了一个叫做“容器”的概念,我们应用程序的对象将由“容器”为我们创建并提供给我们。
在依赖注入的术语当中,对于容器提供的对象我们统称为“服务”,服务包括服务类型和服务实例。
由于依赖注入容器是根据服务类型来获取服务实例的,所以要为某个类型提供服务,则必须先对服务进行注册,注册的服务类型我们通常定义为“接口或基类”以此将依赖关系抽象化。注册时除了服务类型外,还必须指定服务的一个具体实现类,并且注册的时候还需要指定这个服务的生命周期。
应用程序在完成定义和注册工作后,对于服务对象的创建和提供则完全交给框架的“容器”来完成。
“容器”在“机械齿轮”的情景模拟中容器就相当于一个第三方的齿轮,起到了一种“粘合剂”的作用,它不直接参与到应用程序的业务功能代码中,而是由框架负责运行。这样一来,不仅降低了对象之间的耦合程度,还能为各个类型主动提供所依赖的对象。
3.控制反转和依赖注入
在很多地方都看到一种说法是:“依赖注入是实现控制反转的一种方式”。在经过大量的资料查阅之后我们发现,控制反转主要体现的是一种“任务流程控制权”的反转,而依赖注入则是体现的一种“对象创建权”的反转,更为抽象的说法应该是“对象依赖关系”的反转,这表明它们在反转的“事物”上存在着差异,但是它们反转的双方对象都是一致的,即从应用程序中转移到框架中。
所以基于上诉的分析,我个人认为控制反转和依赖注入并不是一种等价的概念,所以说“依赖注入是实现控制反转的一种方式”不是很恰当。这可以从软件开发教父Martin Fowler说发表文章中的一段话中区分开来,其中这段话翻译过来大致的含义如下:
当这些容器谈论它们如何如此有用,因为它们实现了“控制反转”时,我最终感到非常困惑。控制反转是框架的一个共同特征,所以说这些轻量级容器是特殊的,因为它们使用反转控制,就像说我的车是特殊的,因为它有轮子。
对于这些概念性的技术点,其实通过文字是很难下定义的。这不外乎和我们中国的传统文化一样,你问别人何为“道”?“道可道,非常道”,那可能10个人中会有9种解释。
基于客观性,我个人根据学习总结,对控制反转和依赖注入之间的理解是:控制反转是一个相对于笼统的说法,这就像张三开发的Web应用是面向对象的程序,李四开发的WPF桌面应用也属于面向对象程序,面向对象只不过是一个程序设计的基本。而依赖注入也是将“控制反转”做为一个框架的基本思想从而设计出来的一套用于实现“依赖关系反转”的应用框架。
有兴趣的朋友可以查阅Martin Fowler发表的一篇关于控制反转和依赖注入的经典文章进行深入研究:
https://www.martinfowler.com/articles/injection.html#ConcludingThoughts
4.依赖注入的形式
对于每个需要依赖注入容器提供对象的类而言,都需要为依赖注入容器提供注入的形式,也就是告诉容器通过什么样的方式将对象传递给你,只有提前定义好了注入形式,容器才能对其进行依赖对象的注入。
依赖注入对于设计模式层面而言,其中注入的形式分为三种:1.接口注入、2.设置器注入、3.构造函数注入,对于不同的依赖注入框架而言其中的注入形式也存在差异,本文目前只解释.NET Core默认支持的构造函数注入的形式进行介绍。
构造函数注入就是:依赖注入容器将依赖的对象作为构造函数的参数传递到相应的类中。如下面的代码片段所示,Person类中依赖一个IHouse接口类型,而IHouse的实例则通过构造函数中对应类型的参数进行赋值。
1 public class Person
2 {
3 public IHouse House {get;}
4 public Person(IHouse house)
5 {
6 House=house;
7 }
8 }
使用构造器注入方式需要考虑到一个问题,因为构造函数会存在多个,存在多个构造函数情况下,依赖注入容器又会选择哪一个呢?容器对于这种选择情况,实际上在不同的依赖注入框架种会存在不同的选择策略,所以要根据采用的依赖注入框架而定。那么对于.NET Core而言,它会在所有构造函数的参数列表进行查找,看看哪个构造函数的参数列表是一个“超集”,如果存在“超集”那就会选择这个“超集”所对应的构造函数,对于这个超集的逻辑后续会详细展开,这里只做一个初步的了解即可。
1 public class Person 2 { 3 public IHouse House {get;} 4 public ICar Car {get;} 5 6 public Person(IHouse house)=>House=house; 7 public Person(IHouse house,ICar car):this(house)=>car=Car; //超集 8 }
5.直接依赖和间接依赖
依赖注入在提供某个服务对象时,该服务对象中往往存在着对其他服务对象的依赖,服务对象中的字段或属性是依赖的一种主要体现形式。依赖注入容器会根据这种“依赖链”提供所有间接或直接依赖的服务实例。
如果类型A中具有一个B类型的字段或属性,那么就代表类型A对类型B产生了依赖,这就属于一种直接的依赖。如果类型B中还存在一个C类型的字段或属性,那么类型A对类型C产生的依赖属于间接依赖,并且类型C后面间接或直接依赖的类型对于类型A而言都是间接依赖。依赖注入容器在使用的时候,不光对类型A直接依赖的类型进行对象的提供,并且对类型A所有间接依赖的类型也同样会进行对象提供。
例如下图包含了Person的直接依赖和间接依赖:
上图中Computer类对象依赖于Displayer类,所以Displayer类成了Person类的间接依赖。那么对于依赖注入容器而言,如果要提供Person类的对象,那么它直接和间接依赖的对象Computer、Displayer都会预先被初始化并自动注入到Person类的对象之中。基于这种注入的特点,我们可以简单地理解“依赖注入”属于一种针对依赖字段或属性的自动初始化方式。
存在间接依赖的注意事项
如果直接依赖的服务还存在其他依赖的服务(也就是对于当前提供的服务存在间接依赖),那么直接依赖的服务中需要提供对间接依赖服务注入的构造函数,否则这个间接服务会为NULL。
还是以上面的类图为例,如果要提供Person类的服务,Pseson类中就需要提供对Computer服务注入的构造函数。但是由于直接依赖的Computer类还存在对Displayer类的依赖,即Person的间接依赖,那么Computer类中还必须提供对Displayer服务注入的构造函数,不然这个Displayer间接服务的值会为NULL。
6.依赖注入初体验
对于开发某个新的功能,当某个类需要使用外部依赖类型的对象时,使用依赖注入的流程步骤大致如下:
依赖注入这个技术知识点仅从理论上也很难直观的感受到它的“魅力”,接下来我打算通过代码示例的形式,让大家体验感受下依赖注入在ASP.NET Core中简单的应用形式。
我们根据上面的类图作为示例的背景,创建一个ASP.NET Core MVC的应用。针对Home控制器中的依赖项computer字段使用依赖注入的方式创建对象。通常在依赖注入应用场景下,依赖项的类型都定义为接口或基类,所以Home控制器依赖的computer字段类型定义为了一个接口。该接口有一个实现类为DellComputer,它会在服务注册时进行使用,DellComputer类的对象会赋值给computer字段。
接下来我们根据类图实现具体的编码步骤:
1.新建ASP.NET Core MVC的项目,并编码实现相应的类型,由于逻辑比较简单,所以类型都写在一个文件中,代码如下:
1 namespace DependencyInjectionDemo 2 { 3 public interface IComputer 4 { 5 string SayHi(); //打招呼 6 } 7 8 public class Computer : IComputer 9 { 10 public string SayHi() 11 { 12 return "你好,我是戴尔电脑"; 13 } 14 } 15 16 }
2.在Home控制器中将IComputer接口类型的字段作为依赖项,并使用构造函数作为依赖注入的方式,然后在控制器的Index方法中使用依赖项中的SayHi方法,将该方法的返回值输出到视图。
1 public class HomeController : Controller 2 { 3 private readonly IComputer computer; 4 5 public HomeController(IComputer computer) 6 { 7 this.computer = computer; 8 } 9 10 public IActionResult Index() 11 { 12 ViewBag.Msg = this.computer.SayHi(); 13 return View(); 14 } 15 16 }
3.在Index视图中输出后台控制器中设置的ViewBag数据,该数据来源于Home控制器的computer字段,也就是它的依赖项。
4.在Startup.cs类的ConfigureServices方法中对Person类所依赖的IComputer服务进行注册。使用AddSingleton进行服务注册,第一个泛型参数为服务类型,第二个泛型参数为服务的实现类。该方法还决定了服务对象的生命周期,对于生命周期后面会有专题进行详细说明,目前无需纠结。此步骤实际上就是告诉容器,我的应用程序需要使用IComputer服务,并且具体的服务实现类是Computer类。
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddSingleton<IComputer, Computer>(); 4 }
5.在完成了服务注册后,对于当前示例的依赖注入运用已经构建完成,我们可以运行项目查看效果。
上图中成功的输出了Home控制器依赖项(computer字段)的方法,并且在Home控制器中使用computer字段并没有通过“new”的形式对其实例化,就实现了对一个引用类型的方法调用。实际操作体验后你会发现,在ASP.NET Core中使用依赖注入并没有什么难点,但这其中的作用其实都是归功于依赖注入框架。
7.结语
依赖注入通常对于一些初学者来说,它们在实际的项目中都是“无感”的,并且在面对日常的开发工作而言使用起来也很简单。这种现象本身就是一个优秀框架的设计体现之处,让一些复杂的东西从应用程序代码中转移到框架中,这会让运用框架的应用程序变得易于开发、易于扩展。
本文内容只能依赖注入做一个基本的介绍,如果想要完全掌握依赖注入框架这只是一个开头,后续在ASP.NET Core应用方面还有很多的细节点,例如服务的生命周期模式、注册和消费、反模式等等。
虽然依赖注入在应用程序中不会涉及很多代码量,但是它是ASP.NET Core框架的基石,整个ASP.NET Core都建立在一个依赖框架之上。所以掌握好依赖注入是你对ASP.NET Core起码的诚意,也是.NET开发者基本的素质。