12.1 介绍DI 容器
DI容器是一个软件库,它可以将对象构成、生命周期管理和拦截中涉及的许多任务自动化。 尽管用Pure DI编写所有需要的基础结构代码是可能的,但它并不能为应用程序增加多少价值。另一方面,构成对象的任务具有普遍性,可以一劳永逸地解决;这就是所谓的通用子域。这与实现日志或数据访问没有什么不同;日志应用数据是那种可以由通用日志库解决的问题。组成对象图的情况也是如此。
在本节中,我们将讨论DI容器如何组成对象图。我们还将向你展示一些例子,让你大致了解使用容器和实现可能是什么样子。
12.1.1 探索容器的解析API
一个DI容器是一个像其他软件库一样的软件库。它暴露了一个API,你可以用它来组合对象,组合一个对象图是一个单一的方法调用。DI容器也需要你在合成对象之前对其进行配置。我们将在第12.2节重新讨论这个问题。
在这里,我们将向你展示一些关于DI容器如何解决对象图的例子。作为本节的例子,我们将使用Autofac和Simple Injector应用于一个ASP.NET Core MVC应用程序。关于如何组成ASP.NET Core MVC应用程序的更多详细信息,请参考7.3节。
你可以使用一个DI容器来解析控制器实例。这个功能可以用下面几章中涉及的所有三种DI容器来实现,但我们在这里只展示几个例子。
用各种di容器解决控制器的问题
Autofac是一个DI容器,有一个相当符合模式的API。假设你已经有一个Autofac容器实例,你可以通过提供请求的类型来解决一个控制器。
var controller = (HomeController)container.Resolve(typeof(HomeController));
你将把typeof(HomeController)传给Resolve方法,并得到一个所要求的类型的实例,其中包含所有适当的依赖。 Resolve方法是弱类型的,它返回一个System.Object的实例;这意味着你需要将其转换为更具体的东西,如本例所示。
许多DI容器的API都与Autofac的相似。Simple Injector的相应代码看起来与Autofac的几乎相同,尽管实例是用SimpleInjector.Container类来解决的。使用Simple Injector,之前的代码看起来是这样的:
controller = (HomeController)container.GetInstance(typeof(HomeController));
唯一真正的区别是,Resolve方法被称为GetInstance。 你可以从这些例子中提取出一个DI容器的一般形状。
用DI容器解决对象图的问题
一个DI容器是一个解析和管理对象图的引擎。 尽管DI容器比解析对象的内容更多,但这是任何容器的API的核心部分。前面的例子表明,容器有一个弱类型的方法来实现这一目的。随着名称和签名的变化,这个方法看起来像这样:
object Resolve(Type serviceType);
正如前面的例子所示,由于返回的实例类型是System.Object,所以在使用它之前,你经常需要把返回值投射到预期的类型。许多DI容器也为那些你在编译时就知道要请求哪种类型的情况提供了一个通用版本。它们通常看起来像这样。
T Resolve<T>();
这种重载不是提供一个Type方法参数,而是接受一个类型参数(T),表示请求的类型。该方法返回一个T的实例。如果不能解决请求的类型,大多数容器会抛出一个异常。
警告 Resolve方法的签名是非常强大和通用的。你可以请求任何类型的实例,你的代码仍然可以编译。事实上,Resolve方法适合于服务定位器的签名。正如在5.2中所讨论的,你需要注意不要在组合根之外调用Resolve把你的DI容器当作了一个服务定位器。
如果我们孤立地看待Resolve方法,它几乎看起来像魔术。 从编译器的角度来看,我们可以要求它解析任意类型的实例。容器如何知道如何组成所请求的类型,包括所有的依赖?它不知道;你必须先告诉它。你可以使用一个将抽象映射到具体类型的配置来这样做。我们将在第 12.2 节回到这个话题。
如果一个容器没有足够的配置来完全组成一个请求的类型,它通常会抛出一个描述性的异常。 作为一个例子,考虑一下我们在列表 3.4 中首次讨论的下面这个 HomeController。你可能还记得,它包含一个 IProductService 类型的依赖。
public class HomeController: Controller {
private readonly IProductService productService;
public HomeController(IProductService productService) {
this.productService = productService;
}
...
}
在不完整的配置下,Simple Injector有像这样的示范性异常信息。
HomeController的构造函数包含了一个名为 "productService "的参数,其类型为IProductService,但该参数并未注册。请确保IProductService被注册,或者改变HomeController的构造函数。
在前面的例子中,你可以看到Simple Injector不能解析HomeController,因为它包含一个IProductService类型的构造参数,但是Simple Injector并没有被告知在请求IProductService时应该返回哪个实现。如果容器配置正确,它甚至可以从请求的类型中解析复杂的对象图。 如果配置中缺少什么,容器可以提供关于缺少什么的详细信息。在下一节中,我们将仔细看看这是如何做到的。
12.1.2 自动布线
DI容器依靠编译到所有类中的静态信息茁壮成长。使用反射,他们可以分析所请求的类并找出需要的依赖。
正如第4.2节所解释的,构造函数注入是应用DI的首选方式,正因为如此,所有的DI容器都天生理解构造函数注入。具体来说,它们通过将自己的配置与从类的类型信息中提取的信息相结合来构成对象图。这就是所谓的自动布线。
定义 自动布线是利用编译器和通用语言运行时(CLR)提供的类型信息,从抽象和具体类型之间的映射中自动构成一个对象图的能力。
大多数DI容器也理解属性注入,尽管有些需要你明确地启用它。 考虑到属性注入的缺点(如第4.4节所解释的),这是件好事。图12.2描述了大多数DI容器在自动连接对象图时遵循的一般算法。
图 12.2 自动布线的简化工作流程。di容器使用它的配置来寻找符合请求类型的适当的具体类。然后,它使用反射来检查该类的构造函数。
如图所示,DI容器为一个被请求的抽象找到具体的类型。如果具体类型的构造器需要参数,一个递归过程就开始了,DI容器为每个参数类型重复这一过程,直到所有的构造器参数都得到满足。当这一过程完成后,容器构造具体类型,同时注入递归解决的依赖关系。
注意 大多数 DI 容器实现了优化,使连续的请求能够更快地执行。这些优化的执行方式因容器而异。正如我们在第 4.2.2 节中提到的,DI 容器通常不会给你的应用程序带来明显的性能开销。I/O是普通应用程序最重要的瓶颈,优化I/O通常比优化Object Composition产生更多的收益。
在第12.2节中,我们将仔细研究如何配置容器。现在,最重要的是要明白,配置的核心是抽象和其代表的具体类之间的映射列表。这听起来有点理论性,所以我们认为一个例子会有帮助。