依赖关系注入

提示

此内容摘自电子书《使用 .NET MAUI 的企业应用程序模式》,可在 .NET 文档上获取,也可作为免费可下载的 PDF 脱机阅读。

Enterprise Application Patterns Using .NET MAUI eBook cover thumbnail.

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

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

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

依赖关系注入简介

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

private readonly ISettingsService _settingsService;
private readonly IAppEnvironmentService _appEnvironmentService;

public ProfileViewModel(
    IAppEnvironmentService appEnvironmentService,
    IDialogService dialogService, 
    INavigationService navigationService, 
    ISettingsService settingsService)
    : base(dialogService, navigationService, settingsService)
{
    _appEnvironmentService = appEnvironmentService;
    _settingsService = settingsService;

    // Omitted for brevity
}

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

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

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

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

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

.NET 中有许多可用的依赖关系注入容器;eShopOnContainers 多平台应用使用 Microsoft.Extensions.DependencyInjection 来管理应用中视图、视图模型和服务类的实例化。 Microsoft.Extensions.DependencyInjection 有助于生成松散耦合的应用,并提供依赖关系注入容器中常见的所有功能,包括注册类型映射和对象实例、解析对象、管理对象生存期以及将依赖对象注入它解析的对象的构造函数的方法。 有关 Microsoft.Extensions.DependencyInjection 的详细信息,请参阅 .NET 中的依赖关系注入

在 .NET MAUI 中,MauiProgram 类将调用 CreateMauiApp 方法来创建 MauiAppBuilder 对象。 MauiAppBuilder 对象具有 IServiceCollection 类型的 Services 属性,该属性提供了一个注册组件的位置,例如用于依赖关系注入的视图、视图模型和服务。 调用 MauiAppBuilder.Build 方法时,将向依赖关系注入容器提供使用 Services 属性注册的任何组件。

在运行时,容器必须知道正在请求哪些服务实现,以便为请求的对象实例化这些服务。 在 eShopOnContainers 多平台应用中,需要先解析 IAppEnvironmentServiceIDialogServiceINavigationServiceISettingsService 接口,然后才能实例化 ProfileViewModel 对象。 这涉及到容器执行以下操作:

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

最终,应用使用完 ProfileViewModel 对象后,该对象将用于垃圾回收。 此时,如果其他类不共享同一个实例,垃圾回收器应释放任何短生存期的接口实现。

注册

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

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

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

注意

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

需要依赖关系注入的类型的注册应该在应用中的单个方法中执行。 此方法应在应用生命周期的早期调用,以确保它了解其类之间的依赖关系。 eShopOnContainers 多平台应用通过 MauiProgram.CreateMauiApp 方法执行此操作。 以下代码示例显示了 eShopOnContainers 多平台应用如何在 MauiProgram 类中声明 CreateMauiApp

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
        => MauiApp.CreateBuilder()
            .UseMauiApp<App>()
            // Omitted for brevity            
            .RegisterAppServices()
            .RegisterViewModels()
            .RegisterViews()
            .Build();
}

MauiApp.CreateBuilder 方法创建一个 MauiAppBuilder 对象,可用于注册依赖项。 eShopOnContainers 多平台应用中的许多依赖项都需要注册,因此创建了扩展方法 RegisterAppServicesRegisterViewModelsRegisterViews 以帮助提供有组织且可维护的注册工作流。 下面的代码演示了 RegisterViewModels 方法:

public static MauiAppBuilder RegisterViewModels(this MauiAppBuilder mauiAppBuilder)
{
    mauiAppBuilder.Services.AddSingleton<ViewModels.MainViewModel>();
    mauiAppBuilder.Services.AddSingleton<ViewModels.LoginViewModel>();
    mauiAppBuilder.Services.AddSingleton<ViewModels.BasketViewModel>();
    mauiAppBuilder.Services.AddSingleton<ViewModels.CatalogViewModel>();
    mauiAppBuilder.Services.AddSingleton<ViewModels.ProfileViewModel>();

    mauiAppBuilder.Services.AddTransient<ViewModels.CheckoutViewModel>();
    mauiAppBuilder.Services.AddTransient<ViewModels.OrderDetailViewModel>();
    mauiAppBuilder.Services.AddTransient<ViewModels.SettingsViewModel>();
    mauiAppBuilder.Services.AddTransient<ViewModels.CampaignViewModel>();
    mauiAppBuilder.Services.AddTransient<ViewModels.CampaignDetailsViewModel>();

    return mauiAppBuilder;
}

此方法接收 MauiAppBuilder 的实例,可以使用 Services 属性来注册视图模型。 根据应用程序的需求,可能需要添加具有不同生存期的服务。 下表提供有关何时可能需要选择这些不同注册生存期的信息:

方法 说明
AddSingleton<T> 将创建该对象的单个实例,该实例将保留在应用程序的生存期内。
AddTransient<T> 如果在解析期间请求,将创建对象的新实例。 暂时性对象没有预定义的生存期,但通常遵循其主机的生存期。

注意

视图模型不从接口继承,因此它们只需要将其具体类型提供给 AddSingleton<T>AddTransient<T> 方法。

CatalogViewModel 在应用的根附近使用,并且应始终可用,因此向 AddSingleton<T> 注册它是有益的。 其他视图模型(例如 CheckoutViewModelOrderDetailViewModel)会根据情况导航到应用程序或稍后在应用程序中使用。 假设你知道你有一个可能并不总是使用的组件。 在这种情况下,如果它是内存或计算密集型组件或需要即时数据,它可能更适合用于 AddTransient<T> 注册。

添加服务的另一种常用方法是使用 AddSingleton<TService, TImplementation>AddTransient<TService, TImplementation> 方法。 这些方法采用两种输入类型:接口定义和具体实现。 这种注册类型最适合基于接口实现服务的情况。 在下面的代码示例中,我们使用 SettingsService 实现注册了 ISettingsService 接口:

public static MauiAppBuilder RegisterAppServices(this MauiAppBuilder mauiAppBuilder)
{
    mauiAppBuilder.Services.AddSingleton<ISettingsService, SettingsService>();
    // Omitted for brevity...
}

注册所有服务后,应调用 MauiAppBuilder.Build 方法来创建 MauiApp 并使用所有注册的服务填充依赖关系注入容器。

重要

一旦调用了 Build 方法,使用依赖关系注入容器注册的服务将不可变,并且不能再更新或修改。

解决方法

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

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

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

.NET MAUI 提供了多种方法来根据需要解析已注册的组件。 访问依赖关系注入容器的最直接方法是使用 Handler.MauiContext.ServicesElement 访问。 下面是它的一个示例:

var settingsService = this.Handler.MauiContext.Services.GetServices<ISettingsService>();

如果需要从 Element 内部或从 Element 的构造函数外部解析服务,这会很有帮助。

注意

ElementHandler 属性可能为 null,因此请注意你可能需要处理这些情况。 有关详细信息,请参阅 Microsoft 文档中心的处理程序生命周期

如果使用 .NET MAUI 的 Shell 控件,它将隐式调用依赖关系注入容器以在导航期间创建对象。 在设置 Shell 控件时,Routing.RegisterRoute 方法会将路由路径绑定到 View,如下例所示:

Routing.RegisterRoute("Filter", typeof(FiltersView));

Shell 导航期间,它将查找 FiltersView 的注册,如果找到,它将创建该视图并将任何依赖项注入构造函数。 如以下代码示例所示,CatalogViewModel 将注入 FiltersView

namespace eShopOnContainers.Views;

public partial class FiltersView : ContentPage
{
    public FiltersView(CatalogViewModel viewModel)
    {
        BindingContext = viewModel;

        InitializeComponent();
    }
}

提示

依赖关系注入容器非常适合用于创建视图模型实例。 如果视图模型具有依赖项,它将处理任何所需服务的创建和注入。 只需确保使用 MauiProgram 类中的 CreateMauiApp 方法注册视图模型和它们可能具有的任何依赖项。

总结

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

Microsoft.Extensions.DependencyInjection 有助于生成松散耦合的应用,并提供依赖关系注入容器中常见的所有功能,包括注册类型映射和对象实例、解析对象、管理对象生存期以及将依赖对象注入它解析的对象的构造函数的方法。