[ASP.NET Core 3框架揭秘] 依赖注入[1]:控制反转
ASP.NET Core框架建立在一些核心的基础框架之上,这些基础框架包括依赖注入、文件系统、配置选项和诊断日志等。这些框架不仅仅是支撑ASP.NET Core框架的基础,我们在进行应用开发的时候同样会频繁地使用到它们。对于这里提到的这几个基础框架,依赖注入尤为重要。ASP.NET Core应用在启动以及后续针对请求的处理过程中,它会依赖各种的组件提供服务。为了便于定制,这些组件一般会以接口的形式进行“标准化”,我们将这些标准化的组件统一称为“服务(Service)”。整个ASP.NET Core框架建立在一个底层的依赖注入框架之上,它使用依赖注入容器来提供所需的服务对象。要了解这个依赖注入容器以及它的服务提供机制,我们得先知道什么是“依赖注入(DI:Dependence Injection)”。一旦我们提到依赖注入,又不得不说说“控制反转(IoC:Inverse of Control)”。
一、流程控制的反转
软件开发中的一些所谓的设计理念往往没有一个明确的定义,比如之前流行的SOA和现在炒得火热的“微服务(Micro Service)”和“无服务器(Serverless)”,我们都没法通过一个明确的“内涵”给它们一个准确的定义,而只能从“外延”上描述这些架构设计应该具有怎样的特性。正因为无法给出一个明确的界定,这造成了人们针对同一个概念往往会有很多不同的理解。针对IoC也是这种情况,所以本章所述的仅仅代表作者的一家之言,读者朋友姑且听之,仅作参考。
我听到很多人将IoC说成是一种“面向对象的设计模式”,但在我个人看来IoC不但不能算作一种“设计模式”,其自身也与“面向对象”没有直接的关系。很多人之所以不能很准确地理解IoC,缘于他们忽略了一个最根本的东西,那就是IoC这个短语本身。
IoC的全名Inverse of Control,翻译成中文就是“控制反转”或者“控制倒置”。控制反转也好,控制倒置也罢,它体现的意思是控制权的转移,即控制权原来在A手中,现在需要B来接管。那么对于软件设计来说,IoC所谓的控制权转移具有怎样的体现呢?要回答这个问题,就需要先了解IoC的C(Control)究竟指的是怎样一种控制。对于我们所在的任何一项任务,不论其大小,基本上都可以分解成相应的步骤,所以任何一项任务的实施都有其固有的流程,而IoC涉及的控制可以理解为“针对流程的控制”。
我们通过一个具体实例来说明传统的设计在采用了IoC之后针对流程的控制是如何实现反转的。比如我们要设计一个针对Web的MVC类库,不妨将其命名为MvcLib。简单起见,这个类库中只包含如下这个同名的静态类。
public static class MvcLib { public static Task ListenAsync(Uri address); public static Task<Request> ReceiveAsync(); public static Task<Controller> CreateControllerAsync(Request request); public static Task<View> ExecuteControllerAsync(Controller controller); public static Task RenderViewAsync(View view); }
MvcLib提供了如上5个方法帮助我们完成整个HTTP请求流程中的5个核心任务。具体来说,ListenAsync方法启动一个监听器并将其绑定到指定的地址进行HTTP请求的监听,抵达的请求通过ReceiveAsync方法进行接收,接收到的请求通过一个Request对象来表示。CreateControllerAsync方法根据接收到的请求解析并激活目标Controller对象。ExecuteControllerAsync方法执行激活的Controller并返回一个表示视图的View对象。RenderViewAsync最终将View对象转换成HTML并作为当前请求响应的内容返回给请求的客户端。
现在我们在这个MvcLib的基础上创建一个真正的MVC应用。我们会发现除了按照MvcLib的规范自定义具体的Controller和View之外,我们还需要自行控制从请求的监听与接收、Controller的激活与执行以及View的最终呈现在内的整个流程,这样一个执行流程反映在如下所示的代码中。
class Program { static async Task Main() { while (true) { var address = new Uri("http://0.0.0.0:8080/mvcapp"); await MvcLib.ListenAsync(address); while (true) { var request = await MvcLib.ReceiveAsync(); var controller = await MvcLib.CreateControllerAsync(request); var view = await MvcLib.ExecuteControllerAsync(controller); await MvcLib.RenderViewAsync(view); } } } }
这个例子体现了如下图所示的流程控制方式(应用的代码完全采用异步的方式来处理请求,为了让流程图显得更加简单,我们在流程图中画成了同步的形式,读者不必纠结这个问题)。我们设计的类库(MvcLib)仅仅通过API的形式提供各种单一功能的实现,作为类库消费者的应用程序(App)则需要自行编排整个工作流程。如果从代码重用的角度来讲,这里被重用的仅限于实现某个环节单一功能的代码,编排整个工作流程的代码并没有得到重用。
但是在真实开发场景下,我们需要的不仅仅是一个能够提供单一API的类库,而是能够直接在上面构建应用的框架。类库(Library)和框架(Framework)的不同之处在于:前者往往只是提供实现某种单一功能的API,而后者则针对一个目标任务对这些单一功能进行编排形成一个完整的流程,并利用一个引擎驱动这个流程自动执行。
对于我们上面演示的MvcLib来说,作为消费者的应用程序需要自行控制整个HTTP请求的处理流程,但这实际上这是一个很“泛化”的工作流程,几乎所有的MVC应用均采用这样的流程来监听、接收请求并最终对请求予以响应。如果我们将这个流程实现在一个MVC框架之中,由它构建的所有MVC应用就可以直接使用这个请求处理流程,不需要作无谓的DIY(Do It Yourself)。
现在我们将MvcLib从类库改造成一个框架,姑且将其称为MvcFrame。如下图所示,MvcFrame的核心是一个被称为MvcEngine的执行引擎,它驱动一个编排好的工作流对HTTP请求进行一致性处理。如果我们利用MvcFrame构建一个具体的MVC应用,除了根据我们的业务需求定义相应的Controller和View之外,我们只需要初始化这个引擎并直接启动它即可。如果你曾经开发过ASP.NET MVC应用,你会发现ASP.NET MVC就是这么一个框架。
有了前面演示的这个例子作为铺垫,我们应该很容易理解IoC所谓的控制反转本质上说的是什么了。总的来说,IoC是我们设计框架所采用的一种基本思想,所谓的控制反转就是将应用对流程的控制转移到框架中。拿前面这个例子来说,在传统面向类库编程的时代,针对HTTP请求处理的流程牢牢控制在应用程序手中。在引入框架之后,请求处理的控制权转移到了框架手中。
二、好莱坞法则
在好莱坞,演员把简历递交给演艺公司后就只有回家等待。由于演艺公司对整个娱乐项目具有完全控制权,演员只能被动地接受电影公司的邀约。“不要给我们打电话,我们会给你打电话(Don‘t call us, we‘ll call you)”这是著名的好莱坞法则( Hollywood Principle或者 Hollywood Low),IoC完美地体现了这一法则。
在IoC的语境中,框架就像是掌握整个电影制片流程的电影公司,由于它是整个工作流程的实际控制者,所以只有它知道哪个环节需要哪些人员。应用程序就像是演员,它只需要按照框架定制的规则注册这些组件就可以了,因为框架会在适当的时机自动加载并执行注册的组件。
以熟悉的ASP.NET MVC应用开发来说,我们只需要按照约定的规则(比如约定的目录结构和文件与类型命名方式等)定义相应的Controller类型和View文件就可以了。当ASP.NET MVC框架在处理请求的过程中,它会根据路由解析生成参数得到目标Controller的类型,然后自动创建Controller对象并执行它。如果目标Action方法需要呈现一个View,框架会根据预定义的目录约定找到对应的View文件(.cshtml文件),并对它实施动态编译生成对应的类型。当目标View对象创建出来之后,它执行之后生成的HTML会作为响应回复给客户端。可以看出,整个请求流程处处体现了“框架Call应用”的好莱坞法则。
总的来说,我们在一个框架的基础上进行应用开发,就相当于在一条调试好的流水线上生产某种商品。我们只需要在相应的环节准备对应的原材料,最终下线的就是我们希望得到的产品。IoC几乎是所有框架均具有的一个固有属性,从这个意义上讲,“IoC框架”的说法其实是错误的,世界上并没有什么IoC框架,或者说所有的框架都是IoC框架。
三、流程定制
我们采用IoC实现了流程控制从应用程序向框架的转移,但是被转移的仅仅是一个泛化的流程,任何一个具体的应用都可能需要对该流程的某些环节进行定制。还是以我们的MVC框架来说,默认实现的请求处理流程可以只考虑针对HTTP 1.1的支持,但是我们在设计框架的时候应该提供相应的扩展点来支持HTTP 2。作为一个Web框架,用户认证功能是必备的,但是框架自身不能限制于某一种或者几种固定的认证方式,它应该允许我们通过扩展实现任意的认证模式。
我们可以说得更加宽泛点。如下图所示,我们将一个泛化的工作流程(A=>B=>C)定义在框架之中,建立在该框架的两个应用需要对组成这个流程的某些环节进行定制。比如步骤A和C可以被App1重用,但是步骤B却需要被定制(B1)。App2则重用步骤A和B,但是需要按照自己的方式处理步骤C。
IoC将对流程的控制从应用程序转移到框架之中,框架利用一个引擎驱动整个流程的自动化执行。应用程序无需关心工作流程的细节,它只需要启动这个引擎即可。这个引擎一旦被启动,框架就会完全按照预先编排好的流程进行工作,如果应用程序希望整个流程按照自己希望的方式被执行,需要在启动之前对流程进行定制。一般来说,框架会以相应的形式提供一系列的扩展点,应用程序通过注册扩展的方式实现对流程某个环节的定制。在引擎被启动之前,应用程序将所需的扩展注册到框架之中。一旦引擎被正常启动,这些注册的扩展会自动参与到整个流程的执行过程中。
综上所述,IoC一方面通过流程控制从应用程序向框架的反转实现了针对流程自身的重用,另一方面通过内置的扩展机制使这个被重用的流程能够自由地被定制,这两个因素决定了框架自身的价值。重用让框架不仅仅是为应用程序提供实现单一功能的API,而是提供一整套可执行的解决方案,可定制则使我们可以为不同的应用程序对框架进行定制,这无疑让框架可以使用到更多的应用之中。
[ASP.NET Core 3框架揭秘] 依赖注入[1]:控制反转
[ASP.NET Core 3框架揭秘] 依赖注入[2]:IoC模式
[ASP.NET Core 3框架揭秘] 依赖注入[3]:依赖注入模式
[ASP.NET Core 3框架揭秘] 依赖注入[4]:一个迷你版DI框架
[ASP.NET Core 3框架揭秘] 依赖注入[5]:利用容器提供服务
[ASP.NET Core 3框架揭秘] 依赖注入[6]:服务注册
[ASP.NET Core 3框架揭秘] 依赖注入[7]:服务消费
[ASP.NET Core 3框架揭秘] 依赖注入[8]:服务实例的生命周期
[ASP.NET Core 3框架揭秘] 依赖注入[9]:实现概述
[ASP.NET Core 3框架揭秘] 依赖注入[10]:与第三方依赖注入框架的适配