文檔目錄
本節內容:
什麼是依賴注入
如果你已經知道依賴注入概念、構造器和屬性注入,你可以跳到下一主題。
維基百科:“依賴注入是軟件設計模式,一個依賴對象(或客戶端)需要一個或多個依賴(或服務)的注入,或傳入引用作為它狀態的一部分。該模式把客戶端的依賴的創建和客戶端行為分離開來,這樣使得程序設計更加地松藕合,更符合依賴倒置及單一職責原則。它與客戶端了解依賴關系的服務定位器模式形成對比。”
如果不使用依賴注入技術,很難管理依賴並開發出一個模塊和結構良好的應用。
傳統方式的問題
在一個應用裡,類之間相互依賴。假設我們有一個應用服務,它使用倉儲插入實體到數據庫,在這種情況下,應用服務類依賴於倉儲類,如下:
public class PersonAppService { private IPersonRepository _personRepository; public PersonAppService() { _personRepository = new PersonRepository(); } public void CreatePerson(string name, int age) { var person = new Person { Name = name, Age = age }; _personRepository.Insert(person); } }
PersonAppService使用PersonReopsitory插入一個Person到數據庫。這段代碼的問題:
為克服這些問題,可以使用工廠模式。把倉儲的創建抽象出來。代碼如下:
public class PersonAppService { private IPersonRepository _personRepository; public PersonAppService() { _personRepository = PersonRepositoryFactory.Create(); } public void CreatePerson(string name, int age) { var person = new Person { Name = name, Age = age }; _personRepository.Insert(person); } }
PersonRepositoryFactory是一個靜態類,創建並返回一個IPersonRepository。這就是知名的服務定位器模式。創建的問題解決了,PersonAppService服務不知道創建IPersonRepository的實現,也不再依賴於PersonRepository這個實現。但仍有些問題:
解決方案
在依賴注入上有幾種最佳實踐(模式)。
構造器注入模式
上面的示例代碼可以改寫成下面這樣:
public class PersonAppService { private IPersonRepository _personRepository; public PersonAppService(IPersonRepository personRepository) { _personRepository = personRepository; } public void CreatePerson(string name, int age) { var person = new Person { Name = name, Age = age }; _personRepository.Insert(person); } }
這就是知名的構造器注入模式。此時,PersonAppService代碼,不知道哪個類實現了IPersonRepository和如何創建它。如果要使用PersonAppService,首先創建一個IpersonRepository,然後把它傳遞給PersonAppService的構造器,如下所示:
var repository = new PersonRepository(); var personService = new PersonAppService(repository); personService.CreatePerson("Yunus Emre", 19);
構造器注入是一個完美的方式,創建一個類獨立於依賴對象的創建。但是,上述代碼也有些問題:
幸運地是:有依賴注入框架能自動管理依賴。
屬性注入模式
構造器注入為一個類的依賴的注入提供了一個完美的方式,這種方式你不能創建一個不提供依賴的類的實例,同樣它是必須顯式聲明自身所有需要才能正確工作的強方式。
但是,在某些情況下,有些類依賴其它類,但也可以在不提供依賴的情況下工作,這在橫切關注點(如日志)裡經常遇到,一個類可以在沒有日志的情況下工作,但當你提供一個日志記錄器給它時,它也能寫日志。在這種情況下,你可以定義一個公開的依賴屬性,而不是構造器。考慮一下,我們想在PersonAppService裡寫日志,我們可以像下面這樣改一下:
public class PersonAppService { public ILogger Logger { get; set; } private IPersonRepository _personRepository; public PersonAppService(IPersonRepository personRepository) { _personRepository = personRepository; Logger = NullLogger.Instance; } public void CreatePerson(string name, int age) { Logger.Debug("Inserting a new person to database with name = " + name); var person = new Person { Name = name, Age = age }; _personRepository.Insert(person); Logger.Debug("Successfully inserted!"); } }
NullLogger.Instance是一個單例對象,實現了ILogger,但實現上什麼都不干(不寫日志。它用一個空的方法體實現ILogger)。所以此時,如果你在創建PersonAppService對象後,給它設置一個日志記錄器,PersonAppService就可以寫日志,如下所示:
var personService = new PersonAppService(new PersonRepository()); personService.Logger = new Log4NetLogger(); personService.CreatePerson("Yunus Emre", 19);
假設Log4NetLogger實現了ILogger,並用它Log4Net庫寫日志,因此PersonAppService就能寫日志了。如果我們不設置日志記錄器,它就不寫日志。所以我們就可以說PersonAppService的ILogger是一個可選的依賴。
幾乎所有的依賴注入框架都支持屬性注入。
依賴注入框架
有很多的能自動解析依賴的依賴注入框架,它們可以創建所有依賴的對象(遞歸的依賴),所以你只需要寫好構造器或屬性注入模式,DI(依賴倒置)框架會處理剩下的工作。你的類甚至可以獨立於DI框架,在你的整個應用裡,只有少數的幾行代碼或類顯式的與DI框架交互。
ABP使用Castle Windsor作為依賴注入框架。它是一個最成熟的DI框架。還有很多其它的框架,例如Unity、Ninject、StructureMap、Autofac等。
用依賴注入框架時,你先要注冊你的接口/類到依賴注入框架裡,接著你就可以解析(創建)一個對象了。在Castle windsor裡,代碼類似於下面:
var container = new WindsorContainer(); container.Register( Component.For<IPersonRepository>().ImplementedBy<PersonRepository>().LifestyleTransient(), Component.For<IPersonAppService>().ImplementedBy<PersonAppService>().LifestyleTransient() ); var personService = container.Resolve<IPersonAppService>(); personService.CreatePerson("Yunus Emre", 19);
我們首先創建WindsorContainer容器,接著用它們的接口注冊PersonRepository和PersonAppService,然後我們要求容器創建一個IPersonAppService,它就會用依賴創建PersonAppService並返回。在這個簡單的示例裡使用DI框架,可能不能明顯得看出好處來,但是考慮一下,如果你在一個真實的企業應用遇到很多類和依賴,此時情況就不同了。當然,可以在使用前的其它地方注冊依賴,也可以在一個應用啟動時只注冊一次。
注冊我們同時把對象生命周期(life cycle)聲明為短暫的(transient),這就意味著,當我們解析這個類型的對象時,就會創建一個新的實例。還有一些其它不同的生命周期(如單例)。
ABP依賴注入基礎
當你按照最佳實踐和一些約定寫你的應用時,ABP已經幾乎無形的使用了依賴注入框架。
注冊依賴
在ABP裡有多種不同的方法,把類注冊到依賴注入系統裡。大部分情況,約定注冊就已足夠。
約定注冊
ABP會按照約定自動注冊所有倉儲、領域服務、應用服務、Mvc控制器和Web Api控制器。例如,你有一個IPersonAppService接口和一個實現了該接口的PersonAppService類:
public interface IPersonAppService : IApplicationService { //... } public class PersonAppService : IPersonAppService { //... }
ABP會自動注冊它,因為它實現了IApplicationService接口(空的接口)。注冊成暫時的(每處使用創建一個實例)。當你把IPersonAppService接口注入(用構造器注入)到一個類時,將會創建一個PersonAppService對象並自動傳入構造器。
命名約定:很重要,例如你可以把PersonAppService改成MyPersonAppService或其它以“PersonAppService”為後綴的名稱,由於IPersonAppService也是這個後綴,所以沒有問題。但是你不能把它命名為”service“,如果你這麼做了,IPersonAppService就不能自動注冊了(自注冊到ID框架,而不是用接口),所以,你只能手動注冊。
ABP可以按照約定注冊程序集,你可以告訴ABP按照約定注冊你的程序集,它相當容易:
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
Aseembly.GetExecutingAssembly()獲取包含此代碼的程序集的一個引用。你也可以傳遞一個其它程序集給RegisterAssemblyByConvention方法,通常情況下,這些工作在一個模塊開始初始化時就都已完成。更多信息請查看模塊系統。
你可以通過實現IConventionalRegisterer接口,寫你自己的約定注冊類,然後在你的模塊的預初始化裡,調用IocManager.AddConventionalRegisterer方法,添加你的類。
輔助接口
你可能想注冊一個特定的但不符合約定注冊規則的類,ABP提供了捷徑:ITransientDependency和ISingletonDependency接口。例如:
public interface IPersonManager { //... } public class MyPersonManager : IPersonManager, ISingletonDependency { //... }
用這種方式,你可以很容易地注冊MyPersonManager。當需要注入一個IPersonManager時,就使用到MyPersonManager。注意,依賴被聲明為單例。因此,只創建MyPersonManager的一個實例,並把這同一個實例傳遞給所有需要它的類。在首次使用時創建,並在應用的整個生命周期中使用。
自定義/直接 注冊
如果約定注冊無法完全符合你的情況,你可以使用IocManager或Castle Windsor,注冊你的類和依賴。
使用IocManager
你可以用IocManager注冊依賴(一般在你的模塊定義類的預初始化裡):
IocManager.Register<IMyService, MyService>(DependencyLifeStyle.Transient);
使用Castle Windsor API
你可以使用IIocManger.IocContainer屬性訪問Castle Windsor容器並注冊依賴。例如:
IocManager.IocContainer.Register(Classes.FromThisAssembly().BasedOn<IMySpecialInterface>().LifestylePerThread().WithServiceSelf());
更多信息請參考Windsor文檔。
解析
在你的應用某個需要使用IOC(控制反轉)容器(又名為:DI框架)創建對象的地方,注冊把你的類、依賴關系和生命期,告訴IOC容器。ABP提供了幾個解析方式。
構造器和屬性注入
最佳實踐:你可以使用構造器和屬性注入為你的類獲取所需的依賴。你應該在任何可能的使用這種方式。例如:
public class PersonAppService { public ILogger Logger { get; set; } private IPersonRepository _personRepository; public PersonAppService(IPersonRepository personRepository) { _personRepository = personRepository; Logger = NullLogger.Instance; } public void CreatePerson(string name, int age) { Logger.Debug("Inserting a new person to database with name = " + name); var person = new Person { Name = name, Age = age }; _personRepository.Insert(person); Logger.Debug("Successfully inserted!"); } }
IPersonRepository從構造器注入,ILogger從公開屬性注入。用這種方式,你的代碼完全不知道依賴注入系統。這是使用DI系統最適當的方式。
IIocResolver和IIocManager
你可能需要用直接解析你的依賴來代替構造器和屬性注入。這應該盡量避免,但有時卻又無可避免。ABP提供一些易用的注入服務,例如:
public class MySampleClass : ITransientDependency { private readonly IIocResolver _iocResolver; public MySampleClass(IIocResolver iocResolver) { _iocResolver = iocResolver; } public void DoIt() { //Resolving, using and releasing manually var personService1 = _iocResolver.Resolve<PersonAppService>(); personService1.CreatePerson(new CreatePersonInput { Name = "Yunus", Surname = "Emre" }); _iocResolver.Release(personService1); //Resolving and using in a safe way using (var personService2 = _iocResolver.ResolveAsDisposable<PersonAppService>()) { personService2.Object.CreatePerson(new CreatePersonInput { Name = "Yunus", Surname = "Emre" }); } } }
一個應用中一個MySampleClass例子,它用構造器注入IIocResolver並用它解析和釋放對象。Resolve方法有幾個重載可以用來解析,Release用來釋放組件(對象)。如果你手動解析一個對象,記得調用Release,否則你的應用可能存在內存洩露的問題。為確保釋放對象,盡可能使用ResolveAsDisposable(如上面例子所示),它在using塊的最後自動調用Release。
如果你想直接使用IOC容器(Castle Windsor)來解析依賴,你可以構造器注入IIocManager並使用IIocManager.IocContainer屬性。如果你在一個靜態的上下文裡,或不可能注入IIocManager,最後的選擇是:你可以在任何地方使用單例對象IocManager.Instance,但這種方式使用你的代碼不易於測試。
另外
IShouldInitialize 接口
有些類需要在第一次使用前初始化,IShouldInitialize有一個Initialize方法,如果你實現了它,那麼在創建你的對象之後(使用之前)就會自動調用你的Initialize方法。當然,你應該注入/解析這個對象,以便這一特性起作用。
Asp.net Mvc 和 Asp.net Web Api 集成
我們必須調用依賴注入系統來解析依賴圖上的根對象。在一個Asp.net Mvc應用裡,它通常是一個Controller(控制器)類。我們同樣也可以在控制器裡用構造器注入和屬性注入模式。當一個請求到達我們的應用,用IOC容器創建控制器和所有依賴遞歸解析。所以由誰來做這件事?由ABP通過擴展Mvc的默認控制器工廠自動完成。類似地,Asp.net Web Api也一樣。而且你也不必關系創建和銷毀對象。
Asp.net Core 集成
暫略
最後提醒
只要你依照上面的規則和結構,ABP簡化和自動使用依賴注入。大部分情況你不用做更多的事,但是只要你需要,你可以直接使用Castle Windsor的所有能力來執行任何任務(像自定義注冊,注入鉤子,攔截器等等)。