Dependency Injection

注意

本电子书于 2017 年春季出版,之后再未更新。 书中有许多内容仍然很有价值,但有些材料已经过时。

通常会在实例化对象时调用类构造函数,并将对象所需的任何值作为参数传递给构造函数。 这是一个依赖项注入的示例,具体称为“构造函数注入”。 对象所需的依赖项注入到构造函数中。

通过将依赖项指定为接口类型,依赖项注入可以将具体类型与依赖于这些类型的代码分离。 它通常使用一个容器来保存接口和抽象类型之间的注册和映射列表,以及实现或扩展这些类型的具体类型。

还有其他类型的依赖项注入,例如属性设置器注入和方法调用注入,但它们不太常见。 因此,本章仅关注如何使用依赖关系注入容器执行构造函数注入。

依赖项注入简介

依赖关系注入是控制反转 (IoC) 模式的一个专用版本,其中被反转的关注点是获取所需依赖项的过程。 通过依赖关系注入,另一个类负责在运行时将依赖项注入到对象中。 下面的代码示例演示如何在使用依赖关系注入时构造 ProfileViewModel 类:

public class ProfileViewModel : ViewModelBase  
{  
    private IOrderService _orderService;  

    public ProfileViewModel(IOrderService orderService)  
    {  
        _orderService = orderService;  
    }  
    ...  
}

ProfileViewModel 构造函数接收由另一个类注入的 IOrderService 实例作为参数。 ProfileViewModel 类中的唯一依赖项是接口类型。 因此,ProfileViewModel 类对于负责实例化 IOrderService 对象的类一无所知。 负责实例化 IOrderService 对象并将其插入 ProfileViewModel 类的类称为“依赖项注入容器”

依赖关系注入容器通过提供一种工具来实例化类实例并根据容器的配置管理它们的生命周期,从而减小了对象之间的耦合度。 在对象创建期间,容器将对象所需的任何依赖项注入其中。 如果尚未创建这些依赖项,则容器将先创建并解析其依赖项。

注意

还可使用工厂手动实现依赖项注入。 但是,使用容器可提供额外的功能,例如生存期管理和通过程序集扫描进行注册。

使用依赖关系注入容器有几个优点:

  • 容器不再需要类去定位其依赖项并管理其生存期。
  • 容器允许映射已实现的依赖项,而不会影响类。
  • 容器通过允许模拟依赖项来促进可测试性。
  • 容器通过允许将新类轻松添加到应用来提高可维护性。

在使用 MVVM 的 Xamarin.Forms 应用的上下文中,依赖项注入容器通常用于注册和解析视图模型,以及注册服务并将它们注入到视图模型中。

有许多可用的依赖项注入容器,eShopOnContainers 移动应用使用 TinyIoC 管理应用中视图模型和服务类的实例化。 在评估了许多不同的容器后选择了 TinyIoC,与大多数熟知的容器相比,它在移动平台上具有更强的性能。 它有助于生成松散耦合的应用,并提供依赖项注入容器中常见的所有功能,包括注册类型映射、解析对象、管理对象生存期以及将依赖对象注入它解析的对象的构造函数的方法。 有关 TinyIoC 的详细信息,请参阅 github.com 上的 TinyIoC

在 TinyIoC 中,TinyIoCContainer 类型提供依赖项注入容器。 图 3-1 显示了使用此容器时的依赖项,该容器实例化 IOrderService 对象并将其注入 ProfileViewModel 类中。

Dependencies example when using dependency injection

图 3-1:使用依赖项注入时的依赖项

在运行时,容器必须知道它应实例化 IOrderService 接口的哪个实现,然后才能实例化 ProfileViewModel 对象。 这涉及到:

  • 决定如何实例化实现 IOrderService 接口的对象的容器。 这称为“注册”
  • 实例化实现 IOrderService 接口的对象和 ProfileViewModel 对象的容器。 这称为“解决方法”

最终,应用使用完 ProfileViewModel 对象后,该对象将用于垃圾回收。 此时,如果其他类不共享同一实例,垃圾回收器应释放 IOrderService 实例。

提示

编写与容器无关的代码。 始终尝试编写与容器无关的代码,以将应用与所使用的特定依赖项容器分离。

注册

在将依赖项注入对象之前,必须先向容器注册依赖项的类型。 注册类型通常涉及将容器传递到接口和实现接口的具体类型。

通过代码在容器中注册类型和对象有两种方法:

  • 向容器注册类型或映射。 如果需要,容器将生成指定类型的实例。
  • 将容器中的现有对象注册为单一实例。 如果需要,容器将返回对现有对象的引用。

提示

依赖关系注入容器并不总是合适的。 依赖关系注入引入了额外的复杂性和要求,这些对小型应用可能不合适或无用。 如果某个类没有任何依赖项,或者不是其他类型的依赖项,则将其放入容器中可能没有意义。 此外,如果一个类有一组依赖项,这些依赖项是该类型不可或缺的并且永远不会改变,则将其放入容器中可能也没有意义。

需要依赖项注入的类型的注册应在应用中的单个方法中执行,并且该方法应在应用生命周期的早期进行调用,以确保应用知道其类之间的依赖项。 在 eShopOnContainers 移动应用中,这由 ViewModelLocator 类执行,该类生成 TinyIoCContainer 对象,并且是应用中唯一保存对该对象的引用的类。 以下代码示例显示了 eShopOnContainers 移动应用如何在 ViewModelLocator 类中声明 TinyIoCContainer 对象:

private static TinyIoCContainer _container;

类型在 ViewModelLocator 构造函数中注册。 这通过首先创建 TinyIoCContainer 实例来实现,该实例在以下代码示例中演示:

_container = new TinyIoCContainer();

然后,类型向 TinyIoCContainer 对象注册,下面的代码示例演示了最常见的类型注册形式:

_container.Register<IRequestProvider, RequestProvider>();

此处显示的 Register 方法将接口类型映射到具体类型。 默认情况下,每个接口注册都配置为单一实例,使每个依赖对象接收相同的共享实例。 因此,容器中只存在一个 RequestProvider 实例,该实例由需要通过构造函数注入 IRequestProvider 的对象共享。

也可直接注册具体类型,而无需从接口类型映射,如以下代码示例所示:

_container.Register<ProfileViewModel>();

默认情况下,每个具体类注册都配置为多实例,使每个依赖对象都接收一个新实例。 因此,解析 ProfileViewModel 时,将创建一个新实例,容器将注入其所需的依赖项。

解决方法

注册类型后,可以将其解析或作为依赖项注入。 在解析类型时,如果容器需要创建一个新实例,它会将任何依赖项注入到该实例中。

通常,在解析某个类型时,会发生以下三种情况之一:

  1. 如果尚未注册该类型,容器将引发异常。
  2. 如果类型已注册为单一实例,容器将返回单一实例。 如果这是首次调用该类型,则容器会根据需要创建它,并维护对它的引用。
  3. 如果该类型未注册为单一实例,则容器将返回一个新实例,并且不维护对它的引用。

以下代码示例显示如何解析以前向 TinyIoC 注册的 RequestProvider 类型:

var requestProvider = _container.Resolve<IRequestProvider>();

在此示例中,要求 TinyIoC 解析 IRequestProvider 类型的具体类型,以及任何依赖项。 通常,当需要特定类型的实例时,将调用 Resolve 方法。 有关控制已解析对象的生存期的信息,请参阅管理已解析对象的生存期

以下代码示例显示 eShopOnContainers 移动应用如何实例化视图模型类型及其依赖项:

var viewModel = _container.Resolve(viewModelType);

在此示例中,要求 TinyIoC 解析所请求视图模型的视图模型类型,并且容器还将解析任何依赖项。 解析 ProfileViewModel 类型时,要解析的依赖项是 ISettingsService 对象和 IOrderService 对象。 由于在注册 SettingsServiceOrderService 类时使用了接口注册,因此 TinyIoC 返回 SettingsServiceOrderService 类的单一实例,然后将其传递给 ProfileViewModel 类的构造函数。 有关 eShopOnContainers 移动应用如何构造视图模型并将其关联到视图的详细信息,请参阅使用视图模型定位符自动创建视图模型

注意

使用容器来注册和解析类型会影响性能,因为容器使用反射来创建每个类型,特别是在应用中为每个页面导航重构依赖关系的情况。 如果存在许多或深度依赖关系,则创建成本会显著增加。

管理已解析对象的生存期

使用具体类注册来注册类型后,TinyIoC 的默认行为是在每次解析类型时或依赖机制将实例注入其他类时创建已注册类型的新实例。 在此场景中,容器不保存对已解析对象的引用。 但是,使用接口注册来注册类型时,TinyIoC 的默认行为是将对象的生存期作为单一实例进行管理。 因此,当容器处于范围内时,实例仍在范围内,而在容器超出范围且被垃圾回收时或在代码显式释放容器时,实例将被释放。

可使用 fluent AsSingletonAsMultiInstance API 方法替代默认 TinyIoC 注册行为。 例如,AsSingleton 方法可与 Register 方法一起使用,使容器在调用 Resolve 方法时创建或返回类型的单一实例。 以下代码示例显示如何指示 TinyIoC 创建 LoginViewModel 类的单一实例:

_container.Register<LoginViewModel>().AsSingleton();

首次解析 LoginViewModel 类型时,容器创建新的 LoginViewModel 对象并维护对其的引用。 在 LoginViewModel 的任何后续解析中,容器返回对之前创建的 LoginViewModel 对象的引用。

注意

释放容器时,将释放注册为单一实例的类型。

总结

依赖项注入使具体类型与依赖于这些类型的代码分离。 它通常使用一个容器来保存接口和抽象类型之间的注册和映射列表,以及实现或扩展这些类型的具体类型。

TinyIoC 是一个轻型容器,与大多数熟知容器相比,它在移动平台上具有更强的性能。 它有助于生成松散耦合的应用,并提供依赖项注入容器中常见的所有功能,包括注册类型映射、解析对象、管理对象生存期以及将依赖对象注入它解析的对象的构造函数的方法。