我們知道依賴注入(DI)是一種實現對象及其協作者或依賴關系之間松散耦合的技術。 ASP.NET Core包含一個簡單的內建容器來支持構造器注入。
我們試圖將DI的最佳實踐帶到.NET Core應用程序中,這表現在以下方面:
- 構造器注入
- 注冊組件
- DI in testing
構造器注入
我們可以通過方法注入、屬性注入、構造器注入的方式來注入具體的實例,一般來說構造器注入的方式被認為是最好的方式,所以在應用程序中將使用構造器注入,請避免使用別的注入方式。一個構造器注入的例子如:
public class CharacterRepository : ICharacterRepository
{
private readonly ApplicationDbContext _dbContext;
public CharacterRepository(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
}
注冊組件到容器
在使用DI之前,需要告訴容器組件之間的對應關系,例如:
container.Register<IAService, AService>();
所以當你使用構造器注入的時候,你告訴構造函數需要注入IAService類型的實例,容器會根據你之前注冊的對應關系創建AService的實例。
看起來一切都很簡單,但在實際應用過程中並沒有這么簡單,試想在一個項目中,組件有成千上萬個,這成千上萬個組件之間的對應關系怎么樣維護?
一個稍微改進點的策略根據這些組件的職責分類,把某一類組件的對應關系抽取成方法:
private void RegisterApplicationServices(Container container)
{
container.Register<IAApplicationService, AApplicationService>();
container.Register<IBApplicationService, BApplicationService>();
//...
}
private void RegisterDomainServices(Container container)
{
container.Register<IADomainService, ADomainService>();
container.Register<IBDomainService, BDomainService>();
//...
}
private void RegisterOtherServices(Container container)
{
container.Register<IDataTimeSource, DataTimeSource>();
container.Register<IUserFetcher, UserFetcher>();
//...
}
這兩個分類有什么特點呢?第一個方法試圖把所有的ApplicationService的組件對應關系匯總在一起,第二個方法試圖把所有的DomainService的組件對應關系匯總在一起,比起之前已經有了很大的進步。不過隨着組件的增加,你需要不斷修改這幾個方法。
基於公共接口來注冊組件
第一個方法已經找到了同一類的組件,既然這些組件的性質是一樣的,就可以用同樣的接口來表示,定義一個空接口用來表示ApplicationService:
public interface IApplicationService {}
public interface IAApplicationService : IApplicationService { //.. }
public interface IBApplicationService : IApplicationService { //.. }
一旦這些組件有了公共特點,嘗試創建下面的擴展:
container.Register(Classes.FromAssembly().BaseOn<IApplicationService>()
.WithDefaultInterface());
這句代碼的意思是顯而易見的,掃描某個程序集,找到所有實現了IApplicationService的類進而把組件的對照關系注冊到了容器中。
當組件擁有多個接口
類是可以擁有多個接口的,在實際開發中,這樣的設計也是很常見的:
public interface IOptions { //... }
public interface IAlipayOptions : IOptions { //... }
public class AlipayOptions: IAlipayOptions { //... }
利用上面介紹的擴展注冊所有Options:
container.Register(Classes.FromAssembly().BaseOn<IOptions>()
.WithDefaultInterface());
嘗試通過下面的構造器注入:
public AlipayPayment(IAlipayOptions alipayOptions) { //... }
工作的很好,沒有問題。但是當我們試圖從容器里拿到所有的IOptions類型:
container.ResolveAll<IOptions>();
你得不到任何IOptions類型的實例,原因在於向容器注冊對應關系的過程是一對一的,我們之前的擴展.WithDefaultInterface()只注冊了AlipayOptions和IAlipayOptions的關系,如果想通過上面的方式拿到所有繼承了IOptions的實例,則需要使用另一個擴展:
container.Register(Classes.FromAssembly().BaseOn<IOptions>()
.WithAllInterfaces());
把注冊文件放在正確的位置
我們通過分層的方式隔離了不同職責的程序集,最終Web/API項目將會引用這些低層的程序集。要想把 Web/API啟動起來,需要把所有程序集定義的組件注冊在Web/API項目的容器中。我們把Web/API這種能夠啟動的程序集叫做客戶端。
所以一個典型的客戶端需要通過下面代碼來注冊DI容器:
container.Register(Classes.FromAssembly().BaseOn<IApplicationService>()
.WithDefaultInterface());
container.Register(Classes.FromAssembly().BaseOn<IDomainService>()
.WithDefaultInterface());
//...
// 還有其他無法用公共接口表示的組件,這些組件可能來自於低層服務
container.Register<IDateTimeSource, DateTimeSource>();
container.Register<IUserFetcher, UserFetcher>();
//...
這段代碼描述了一個現象,Web/API客戶端對低層的組件對應關系一清二楚,違反了Tell, Don't Ask Priciple. 正確的做法是:
Web/API客戶端告訴低層組件,幫我安裝你所在的程序集中所有的組件對應關系。
// 安裝所有
services.Install(FromAssembly.Contains<IApplicationService>());
services.Install(FromAssembly.Contains<IDomainService>());
services.Install(FromAssembly.Contains<IOtherService>());
具體的組件對應關系應該定義在相應的程序集中。
這一節的思想都來源於Windsor Castle。
DI in testing
人們在不斷討論單元測試的各種風格和差異,類似於通過Mock來管理依賴的單元測試被認為是一種反模式。見:To Kill a Mockingtest, 而DI的另一個功能在於便於寫出有價值和有效的單元測試。
當你選擇測試一個組件時,實際上要花很多的時間來准備依賴數據,這是顯而易見的,因為組件並不是獨立存在的。試想如果你能從容器中拿到這個組件,容器就會將所有的依賴關系創建好。
但是問題來了,比如說你的被測試組件依賴了一個能夠給第三方發送請求的組件,這顯然並不是你所期望的,你只需要注冊一個假的事先准備好的組件即可。
對ApplicationServiceTests的組件注冊如下:
container.Install(FromAssembly.Contains<FakedComponentsInstaller>());
//..Register other components that ApplicationService depend on
一個對SearchService的測試如下:
[Fact]
public async void WhenInputDataIsValidShouldGetSearchResult()
{
//Arrage
var searchService = _container.Resolve<ISearchService>();
var searchModel = SearchModelBuilder.Default().Build();
//Act
var result = await searchService.Search(searchModel);
//Assert
result.Count.Should().BeGreaterThan(0);
}
