写在前面: 我之前写过一系列关于. NET Core 依赖注入的文章, 由于. NET Core 依赖注入框架的实现原理发生了很大的改变, 加上我对包括 IoC 和 DI 这些理论层面的东西又有了一些新的理解, 所以我在此基础上写了 8 篇文章详细介绍. NET Core 的 DI. 我将这些文章发布到我的微信公众账号 (大内老 A) 下, 很多人留言说还是博客具有更好的阅读体验, 所以我将在未来 8 天时间将它们同步到这里.
软件设计中由一些所谓的理念都没有一个明确的定义, 比如之前流行的 SOA 和现在炒的火热的微服务 (Micro Service) 和无服务器(Serverless), 我们都不能通过一个明确的 "内涵" 给它们一个准确地定义, 只能从 "外延" 上描述这些架构设计应该具有怎样的特性. 正因为无法给出一个明确的界定, 造成了人们针对同一个概念出现了很多不同的理解. 针对 IoC 也是这种情况, 所以本章所诉的仅仅代表作者的一家之言, 读者朋友姑妄听之, 仅作参考.
一, 流程控制的反转
我听到很多人将 IoC 说成是一种 "面向对象的设计模式", 但在我个人看来 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()
- {
- Uri 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);
- }
- }
- }
这个例子体现了如图 1 所示的流程控制方式 (应用的代码完全采用异步的方式来处理请求, 为了让流程图显得更加简单, 我们在流程图中画成了同步的形式, 读者不必纠结这个问题). 我们设计的类库(MvcLib) 仅仅通过 API 的形式提供某种单一功能的实现, 作为类库消费者的应用程序 (App) 则需要自行编排整个工作流程. 如果从重用的角度来讲, 这里被重用的仅限于实现某个环节单一功能的代码, 编排整个工作流程的代码并没有得到重用.
图 1 流程控制掌握在应用程序手中
但是当我们构建一个应用的时候, 我们需要的不仅仅是一个能够提供单一 API 的类库, 我们希望的理想形式是能够直接在一个现有的框架上构建我们的应用. 类库 (Library) 和框架 (Framework) 的不同之处在于, 前者往往只是提供实现某种单一功能的 API, 而后者则针对一个目标任务对这些单一功能进行编排形成一个完整的流程, 这个流程在一个引擎的驱动下自动执行.
对于我们上面演示 MvcLib 来说, 作为消费者的应用程序需要自行控制整个 HTTP 请求的处理流程, 但这是实际上这是一个很 "泛化" 的工作流程, 几乎所有的 MVC 应用均采用这样的流程监听, 接收请求并最终对请求予以响应. 如果我们将这个流程实现在一个 MVC 框架之中, 由它构建的所有 MVC 应用就可以直接使用这个请求处理流程, 而不需要自行重复实现它.
现在我们将 MvcLib 从类库改造成一个框架, 并姑且将其称为 MvcFrame. 如图 2 所示, MvcFrame 的核心是一个被称为 MvcEngine 的执行引擎, 它驱动一个编排好的工作流对 HTTP 请求进行一致性处理. 如果我们利用 MvcFrame 构建一个具体的 MVC 应用, 除了根据我们的业务需求定义相应的 Controller 和 View 之外, 我们只需要初始化这个引擎并直接启动它即可. 如果你曾经开发过 ASP.NET MVC 应用, 你会发现 ASP.NET MVC 就是这么一个框架.
图 2 流程控制反转到框架手中
有了上面演示的这个例子作为铺垫, 我们应该很容易理解 IoC 所谓的控制反转. 总的来说, IoC 是我们设计框架所采用的一种基本思想, 所谓的控制反转就是将对应用流程的控制转移到框架中. 拿上面这个例子来说, 在传统面向类库编程的时代, 针对 HTTP 请求处理的流程牢牢控制在应用程序手中. 在引入框架之后, 请求处理的控制权转移到了框架手上.
二, 好莱坞法则
在好莱坞, 把简历递交给演艺公司后就只有回家等待. 由演艺公司对整个娱乐项目的完全控制, 演员只能被动式的接受电影公司的工作, 在需要的环节中, 完成自己的演出." 不要给我们打电话, 我们会给你打电话(don't call us, we'll call you)" 这是著名的好莱坞法则( Hollywood Principle 或者 Hollywood Low),IoC 完美地体现了这一法则.
图 3 好莱坞法则
在 IoC 的应用语境中, 框架就像是掌握整个电影制片流程的电影公司, 由于它是整个工作流程的实际控制者, 所以只有它知道哪个环节需要哪些组件. 应用程序就像是演员, 它只需要按照框架定制的规则注册这些组件就可以了, 因为框架会在适当的时机字典加载并执行注册的组件.
以熟悉的 ASP.NET Core MVC 或者 ASP.NET MVC 应用开发来说, 我们只需要按照约定规则 (比如目录结构和命名等) 定义相应的 Controller 类型和 View 文件就可以了. 当 ASP.NET (Core )MVC 框架在进行处理请求的过程中, 它会根据解析生成的路由参数定义为对应的 Controller 类型, 并按照预定义的规则找到我们定义的 Controller, 然后自动创建并执行它. 如果定义在当前 Action 方法需要呈现一个 View, 框架自身会根据预定义的目录约定找到我们定义的 View 文件, 并对它实施动态编译和执行. 整个流程处处体现了 "框架 Call 应用" 的好莱坞法则.
总的来说, 我们在一个框架的基础上进行应用开发, 就相当于在一条调试好的流水线上生成某种商品, 我们只需要在相应的环节准备对应的原材料, 最终下线的就是我们希望得到的最终产品. IoC 几乎是所有框架均具有的一个固有属性, 从这个意义上讲,"IoC 框架" 的说法其实是错误的, 世界上并没有什么 IoC 框架, 或者说几乎所有的框架都是 IoC 框架.
三, 流程定制
我们采用 IoC 实现了流程控制从应用程序向框架自身的反转, 但是这个被反转的仅仅是一个泛化的流程, 任何一个具体的应用都可能需要对组成该流程的某些环节进行定制. 还是以我们的 MVC 框架来说, 可能默认的请求处理流程只考虑到针对 HTTP 1.1 的支持, 但是当我们在设计框架的时候应该提供相应的扩展点来支持 HTTP 2. 作为一个 Web 框架, 用户认证功能是必备的, 但是框架自身不能限制于某一种或者几种固定的认证方式, 应该通过扩展的方式让用户可以自由地定制任意的认证模式.
我们可以说得更加宽泛点. 如图 4 所示, 我们将一个泛化的工作流程 (A=>B=>C) 被定义在框架之中, 建立在该框架的两个应用需要对组成这个流程的某些环节进行定制. 比如步骤 A 和 C 可以被 App1 重用, 但是步骤 B 却需要被定制(B1),App2 则重用步骤 A 和 B, 但是需要按照自己的方式处理步骤 C.
图 4 应用程序对流程的定制
IoC 将对流程的控制从应用程序转移到框架之中, 框架利用一个引擎驱动整个流程的执行, 应用程序无需关心该工作流程的细节, 它只需要启动这个引擎即可. 但是这个引擎一旦被启动, 框架就会完全按照预先编排好的流程进行工作, 如果应用程序希望整个流程按照自己希望的方式被执行, 针对流程的定制一般在发生在启动引擎之前.
一般来说, 框架会以相应的形式提供一系列的扩展点, 应用程序则通过定义扩展的方式实现对流程某个环节的定制. 在引擎被启动之前, 应用程序将所需的扩展注册到框架之中. 一旦引擎被正常启动, 这些注册的扩展会自动参与到整个流程的执行过程中.
综上所述, IoC 一方面通过流程控制从应用程序向框架的反转实现了针对流程自身的重用, 另一方面通过内置的扩展机制这个被重用的流程可能自由地被定制, 这两个因素决定了框架自身的价值. 重用让框架不仅仅是为应用程序提供实现单一功能的 API, 而是提供一整套可执行的解决方案, 可定制则使我们可以为不同的应用程序对框架进行定制, 这无疑让框架可以使用到更多的应用之中.
依赖注入[1]: 控制反转
依赖注入[2]: 基于 IoC 的设计模式
依赖注入[3]: 依赖注入模式
依赖注入[4]: 创建一个简易版的 DI 框架[上篇]
依赖注入[5]: 创建一个简易版的 DI 框架[下篇]
依赖注入[6]: .NET Core DI 框架[编程体验]
依赖注入[7]: .NET Core DI 框架[服务注册]
依赖注入[8]: .NET Core DI 框架[服务消费]
来源: https://www.cnblogs.com/artech/p/net-core-di-01.html