為什麼使用依賴關係註入? 使用 .NET,通過 new 運算符(即,new MyService 或任何想要實例化的對象類型)調用構造函數即可輕鬆實現對象實例化。遺憾的是,此類調用會強制實施客戶端(或應用程式)代碼到已實例化對象的緊密耦合的連接(硬編碼的引用),此外還會引用其程式集/NuGet 包。對 ...
為什麼使用依賴關係註入?
使用 .NET,通過 new 運算符(即,new MyService 或任何想要實例化的對象類型)調用構造函數即可輕鬆實現對象實例化。遺憾的是,此類調用會強制實施客戶端(或應用程式)代碼到已實例化對象的緊密耦合的連接(硬編碼的引用),此外還會引用其程式集/NuGet 包。對於常見的 .NET 類型而言,這不是問題。然而,對於提供“服務”(如日誌記錄、配置、支付、通知或事件 DI)的類型,如果你想切換所用服務的實現,則可能不需要依賴關係。例如,一種方案是,客戶端可能將 NLog 用於日誌記錄,而另一種方案是,客戶端可能選擇 Log4Net 或 Serilog。而且,使用 NLog 的客戶端不喜歡使用 Serilog 打亂其項目,因此,同時引用兩種日誌記錄服務不會令人滿意。
為瞭解決對服務實現的引用進行硬編碼的問題,DI 提供了一個間接層,這樣與其直接使用 new 運算符實例化服務,倒不如客戶端(或應用程式)請求實例的服務集或“工廠”。此外,與其請求特定類型的服務集(例如創建一個緊密耦合的引用),倒不如請求一個介面(如 ILoggerFactory),並期待服務提供程式(本例中為 NLog、Log4Net 或 Serilog)實現該介面。
結果是,當客戶端直接引用抽象程式集 (Logging.Abstractions) 時,會同時定義服務介面,將不需要引用直接實現。
我們將解耦返回到客戶端的實際實例的模式稱為控制反轉。這是因為,與其客戶端確定要實例化的對象,就像使用 new 運算符顯式調用構造函數時一樣,倒不如 DI 確定將返回的內容。DI 註冊了由客戶端請求的類型(一般為介面)和將返回的類型之間的關聯。此外,DI 通常會確定已返回類型的生存期,具體取決於該類型的所有請求之間將有單個共用的實例、每個請求將各有一個新實例,還是介於兩者之間。
對 DI 的一個尤為常見的需求體現在單元測試中。考慮相應地取決於付款服務的購物車服務。假設編寫利用付款服務的購物車服務,並嘗試對購物車服務進行單元測試,而不實際調用真實的付款服務。相反,你想調用的是模擬付款服務。為了使用 DI 實現此目的,你的代碼會從 DI 框架請求付款服務介面的實例而不是調用,例如,new PaymentService。然後,只需為單元測試“配置”DI 框架,以返回一個模擬付款服務。
相比之下,生產主機可以配置購物車,以使用(可能很多)付款服務選項之一。也許最重要的是,引用將僅針對付款抽象,而不是針對每個具體的實現。
提供“服務”的實例而不是使客戶端直接將其實例化是 DI 的基本原則。事實上,一些 DI 框架允許通過支持基於配置和反射的綁定機制(而不是編譯時綁定)從引用實現中對主機進行解耦。這種解耦稱為服務定位器模式。
.NET Core Microsoft.Extensions.DependencyInjection
若要利用 .NET Core DI 框架,你只需引用 Microsoft.Extnesions.DependencyInjection.Abstractions NuGet 包。此包提供了 IServiceCollection 介面的入口,從而公開你可以從中調用 GetService<TService> 的 System.IServiceProvider。類型參數 TService 標識要檢索的服務的類型(一般為介面),如下應用程式代碼獲得了一個實例:
ILoggingFactory loggingFactor = serviceProvider.GetService<ILoggingFactory>();
有一些相應的非泛型 GetService 方法將 Type 作為參數(而不是泛型參數)。泛型方法允許直接分配給特定類型的變數,而非泛型版本需要一個顯式轉換,因為返回類型為 Object。此外,當添加該服務類型時,會有泛型約束,因此使用該類型參數時可以完全避免轉換。
如果在調用 GetService 時沒有使用收集服務註冊任何類型,它將返回 null。這在與 null 傳播運算符結合以將可選行為添加到應用時非常有用。類似的 GetRequiredService 方法在沒有註冊服務類型時會拋出異常。
如你所見,代碼非常簡單。然而,現在缺少的是如何獲得在其上調用 GetService 的服務提供程式的實例。解決方案是首先實例化 ServiceCollection 的預設構造函數,然後再註冊你想要服務提供的類型。圖 1 中顯示了一個示例,你可以假設其中的每個類(Host、Application 和 PaymentService)已在單獨的程式集中實現。此外,儘管 Host 程式集知道要使用哪個記錄器,但是沒有在 Application 或 PaymentService 中引用記錄器。同樣,Host 程式集沒有引用 PaymentServices 程式集。介面也在單獨的“抽象”程式集中實現了。例如,ILogger 介面是在 Microsoft.Extensions.Logging.Abstractions 程式集中定義的。
圖 1 註冊和請求來自依賴關係註入的對象public class Host { public static void Main() { IServiceCollection serviceCollection = new ServiceCollection(); ConfigureServices(serviceCollection); Application application = new Application(serviceCollection); // Run // ... } static private void ConfigureServices(IServiceCollection serviceCollection) { ILoggerFactory loggerFactory = new Logging.LoggerFactory(); serviceCollection.AddInstance<ILoggerFactory>(loggerFactory); } } public class Application { public IServiceProvider Services { get; set; } public ILogger Logger { get; set; } public Application(IServiceCollection serviceCollection) { ConfigureServices(serviceCollection); Services = serviceCollection.BuildServiceProvider(); Logger = Services.GetRequiredService<ILoggerFactory>() .CreateLogger<Application>(); Logger.LogInformation("Application created successfully."); } public void MakePayment(PaymentDetails paymentDetails) { Logger.LogInformation( $"Begin making a payment { paymentDetails }"); IPaymentService paymentService = Services.GetRequiredService<IPaymentService>(); // ... } private void ConfigureServices(IServiceCollection serviceCollection) { serviceCollection.AddSingleton<IPaymentService, PaymentService>(); } } public class PaymentService: IPaymentService { public ILogger Logger { get; } public PaymentService(ILoggerFactory loggerFactory) { Logger = loggerFactory?.CreateLogger<PaymentService>(); if(Logger == null) { throw new ArgumentNullException(nameof(loggerFactory)); } Logger.LogInformation("PaymentService created"); } }
從概念上講,可以將 ServiceCollection 類型認為是名稱/值對,其中名稱是稍後將要檢索的對象的類型(一般為介面),而值是實現介面的類型或用於檢索該類型的演算法(委托)。因此,在圖 1 的 Host.ConfigureServices 方法中調用 AddInstance 可註冊 ILoggerFactory 類型的任何請求,該類型返回在 ConfigureServices 方法中創建的相同 LoggerFactory 實例。因此,Application 和 PaymentService 均可以檢索 ILoggerFactory,而無需瞭解實現和配置記錄器的知識(或程式集/NuGet 引用)。同樣,Application 提供 MakePayment 方法,無需瞭解關於要使用的付款服務的知識。
請註意,ServiceCollection 不直接提供 GetService 或 GetRequiredService 方法。而是由 ServiceCollection.BuildServiceProvider 方法返回的 IServiceProvider 提供這些方法。此外,僅由提供程式提供的服務是調用 BuildServiceProvider 之前添加的服務。
Microsoft.Framework.DependencyInjection.Abstractions 還包括稱為 ActivatorUtilities 的靜態幫助程式類,該類提供了一些有用的方法,用於處理未使用 IServiceProvider(自定義的 ObjectFactory 委托)註冊的構造函數參數,或者在想要創建預設實例的情況下,調用 GetService 時返回 null(請參閱 bit.ly/1WIt4Ka#ActivatorUtilities)。
服務生存期
在圖 1 中,我調用了 IServiceCollection AddInstance<TService>(TService implementationInstance) 擴展方法。Instance 是 .NET Core DI 附帶的四個不同的 TService 生存期選項之一。它規定不僅 GetService 的調用將返回 TService 類型的對象,而且將返回使用 AddInstance 註冊的特定 implementationInstance 實例。換句話說,使用 AddInstance 進行註冊可以保存特定的 implementationInstance 實例,因此每次使用 AddInstance 方法的 TService 類型參數調用 GetService(或 GetRequiredService)時均可以返回該實例。
相反,IServiceCollection AddSingleton<TService> 擴展方法沒有實例參數,而是依賴於通過構造函數進行實例化的 TService。預設的構造函數有效,Microsoft.Extensions.DependencyInjection 也支持註冊了參數的非預設構造函數。例如,你可以調用:
IPaymentService paymentService = Services.GetRequiredService<IPaymentService>()
而且,在實例化需要其構造函數中的 ILoggingFactory 的 PaymentService 類時,DI 將負責檢索具體的 ILoggingFactory 實例並利用該實例。
如果 TService 類型中沒有此類方法可用,則可以重載 AddSingleton 擴展方法,該方法採用了 Func<IServiceProvider, TService> implementationFactory(用於實例化 TService 的工廠方法)類型的委托。無論你是否提供工廠方法,服務收集實現都會確保將僅創建一個 TService 類型的實例,從而確保存在單一實例。在第一次調用觸發 TService 實例的 GetService 後,在服務收集的生存期內將始終返回同一實例。
IServiceCollection 還包括 AddTransient(Type serviceType, Type implementationType) 和 AddTransient(Type serviceType, Func<IServiceProvider, TService> implementationFactory) 擴展方法。這些方法類似於 AddSingleton,不同的是每次調用這些方法時都會返回一個新實例,從而確保你始終擁有 TService 類型的新實例。
最後,有幾個 AddScoped 類型的擴展方法。這些方法設計為在給定的上下文中返回同一實例,並且每當上下文(也稱為作用域)更改時都會創建新實例。從概念上講,ASP.NET Core 的行為映射到作用域生存期。從本質上講,新實例是針對每個 HttpContext 實例創建的,而且每當在相同的 HttpContext 內調用 GetService 時,都會返回完全相同的 TService 實例。
總之,有四個生存期選項,用於從服務收集實現返回的對象: Instance、Singleton、Transient 和 Scoped。最後三個是在 ServiceLifetime 枚舉中定義的 (bit.ly/1SFtcaG)。但是,缺少 Instance,因為它是 Scoped(在其中無法更改上下文)的特殊用例。
之前我提到過 ServiceCollection 在概念上就像一個名稱/值對,它將 TService 類型用於查找。ServiceCollection 類型的實際實現在 ServiceDescription 類中完成(請參閱 bit.ly/1SFoDgu)。該類為實例化 TService(即,ServiceType (TService))、ImplementationType 或 ImplementationFactory 委托以及 ServiceLifetime 所需的信息提供了一個容器。除了 ServiceDescriptor 構造函數,ServiceDescriptor 上還有許多靜態工廠方法,可幫助實例化 ServiceDescriptor 本身。
無論使用哪種生存期註冊 TService,TService 本身必須是一個引用類型,而不是值類型。每當你將類型參數用於 TService(而不是作為參數傳遞 Type)時,編譯器都會使用泛型類約束進行驗證。然而,編譯器不會驗證是否使用的是對象類型 TService。你一定要避免這種情況,以及任何其他非獨特的介面(或許如 IComparable)。原因是,如果你註冊了對象類型的內容,無論你在 GetService 調用中指定哪種類型的 TService,將始終返回註冊為 TService 類型的對象。
DI 實現的依賴關係註入
ASP.NET 利用 DI 的程度之深,事實上,你可以在 DI 框架本身內實現 DI。換句話說,你不限於使用在 Microsoft.Extensions.DependencyInjection 中發現的 DI 機制的 ServiceCollection 實現。相反,只要你有實現 IServiceCollection(在 Microsoft.Extensions.DependencyInjection.Abstractions 中定義,請參閱 bit.ly/1SKdm1z)或IServiceProvider(在 .NET Core lib 框架的 System 命名空間內定義)的類,你就可以替代自己的 DI 框架或利用另外一個完善的 DI 框架,其中包括 Ninject(ninject.org,經過數年的努力維護 @IanfDavis 呼之欲出)和 Autofac (autofac.org)。
淺談 ActivatorUtilities
Microsoft.Framework.DependencyInjection.Abstractions 還包括靜態幫助程式類,該類提供了一些有用的方法,用於處理未使用 IServiceProvider(自定義的 ObjectFactory 委托)註冊的構造函數參數,或者在想要創建預設實例的情況下,調用 GetService 時返回 null。你可以找到一些在 MVC 框架和 SignalR 庫中使用此實用工具類的示例。在第一種情況下,存在一個帶有 CreateInstance<T>(IServiceProvider provider, params object[] parameters) 簽名的方法,允許你針對未註冊的參數使用 DI 框架將構造函數參數傳入到註冊的類型中。你可能還會有性能需求,lambda 函數需要生成已編譯的 lambda 類型。返回 ObjectFactory 的 CreateFactory(Type instanceType, Type[] argumentTypes) 方法在這種情況下可能有用。第一個參數是用戶尋求的類型,而第二個參數是所有的構造函數類型,以匹配你希望使用的第一個類型的構造函數。在其實現中,這些片段都精簡到已編譯的 lambda,多次調用後,性能會相當高。最後,GetServiceOrCreateInstance<T>(IServiceProvider provider) 方法提供了一個簡單方式,用於提供可能已選擇在其他地方註冊的類型的預設實例。這在調用之前允許 DI 的情況下尤為有用,但是,如果未發生這種情況,你會獲得一個回退實現。
總結
與 .NET Core 日誌記錄和配置一樣,.NET Core DI 機制提供了一個相對簡單的功能實現。雖然你不可能找到其他一些框架的更高級的 DI 功能,但 .NET Core 版本是輕量級的,並且是一個很好的入門方式。此外(再如日誌記錄和配置),.NET Core 實現可以被一個更成熟的實現替代。因此,你可能會考慮利用 .NET Core DI 框架作為一個“包裝器”,通過它,將來你可以根據需要插入其他 DI 框架。通過這種方式,你不必定義自己的“自定義”DI 包裝器,但可以利用 .NET Core 的包裝器作為標準,任何客戶端/應用程式都可以為標準的包裝器插入自定義的實現。
關於 ASP.NET Core 需要註意的是,它自始至終都在利用 DI。這無疑是一個重大實踐,在單元測試中嘗試替代庫的模擬實現時,如果你需要它,它會尤為重要。缺點是,並非簡單的調用帶有 new 運算符的構造函數,DI 註冊和 GetService 調用的複雜性是必要的。我不禁想知道,C# 語言是否可以簡化這種複雜性,但是,基於目前的 C# 7.0 設計,要實現這一點並不容易。
原文:https://msdn.microsoft.com/zh-cn/magazine/mt707534.aspx