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