- 高層模塊不該該依賴於底層模塊,兩者都應該依賴於抽象。
- 抽象不該該依賴於細節,細節應該依賴於抽象。
該原則理解起來稍微有點抽象,咱們能夠將該原則通俗的理解爲:"依賴於抽象」。html
該規則告訴咱們,程序中全部的依賴關係都應該終止於抽象類或者接口
,從而達到鬆耦合的目的。由於咱們在應用程序中編寫的大多數具體類都是不穩定的
。咱們不想直接依賴於這些不穩定的具體類。經過把它們隱藏在抽象和接口
的後面,能夠隔離它們的不穩定性。java
一個Button對象會觸發Click方法,當被按下時,會調用Light對象的TurnOn方法,不然會調用Light對象的TurnOff方法。git
這個設計存在兩個問題:github
這個方案對那些須要被Button控制的對象提出了一個約束。須要被Button控制的對象必需要實現ISwitchableDevice接口。web
所爲原則
,只是描述了什麼是對的,可是並無說清楚如何去作。在軟件工程中,咱們常用DI(依賴注入)
來達到這個目的。可是提到依賴注入,人們又會常常提起IoC
這個術語。因此先讓咱們來了解下什麼是IoC。api
IoC的全名是Inverse of Control,即控制反轉。這一術語並非用來描述面向對象的某種原則或者模式,IoC體現爲一種流程控制的反轉,通常用來對框架進行設計。mvc
ReportService是一個用來顯示報表的流程,該流程包括Trim()
,Clean()
,Show()
三個環節。app
public class ReportService { private string _data; public ReportService(string data) { _data = data; } public void Trim(string data) { _data = data.Trim(); } public void Clean() { _data = _data.Replace("@", ""); _data = _data.Replace("-", ""); //...other rules } public void Show() { Console.WriteLine(_data); } }
客戶端經過下面的方式使用該服務:框架
var reportService = new ReportService(input); reportService.Trim(input); reportService.Clean(); reportService.Show();
這樣的一個設計體現了過程式的思考方式,客戶端依次調用每一個環節從而組成了整個報表顯示流程,這樣的代碼體現了:客戶端擁有流程控制權。asp.net
咱們來分析下這段代碼,ReportService提供了3個可重用的Api,正如ReportService的命名同樣,它告訴咱們它是一個服務,咱們只能重用他提供的三個服務,它沒法提供一個打印報表的流程,整個流程是客戶端來控制的。
另外,該設計也違反了tell, Don't ask原則。
打印報表做爲一個可複用的流程,不但能夠提供可複用的流程環節,還能夠提供可複用的流程的定義,當咱們進行框架設計的時候,每每會將整個流程控制定製在框架之中,而後提供擴展點供客戶端定製。這樣的思想體現了流程的全部權從客戶端到框架的反轉。
好比asp.net mvc或者asp.net api框架,內部定義了http消息從請求,model binder,controller的激活,action的執行,返回response
等可複用的流程。同時還提供了每個環節的可擴展點。
利用以上思想,咱們對ReportService從新設計。
採用IoC思想從新設計該報表服務,將原來客戶端擁有的流程控制權反轉在報表服務框架中。ReportService
這樣的命名已經不適合咱們的想法,新的實現不但提供了報表打印的相關服務,同時還提供了一個可複用的流程,所以從新命名爲ReportEngine
。咱們能夠經過模板方法達到此目的:
public class ReportEngine { private string _data; public ReportEngine(string data) { _data = data; } public void Show() { Trim(); Clean(); Display(); } public virtual void Trim() { _data = _data.Trim(); } public virtual void Clean() { _data = _data.Replace("@", ""); _data = _data.Replace("-", ""); } public virtual void Display() { Console.WriteLine(_data); } }
此時的報表服務在Show()
方法中定義好了一組可複用的流程,客戶端只須要根據本身的需求重寫每一個環節便可。客戶端能夠經過下面的方式使用ReportEngine
var reportEngine=new StringReportEngine(input); reportEngine.Show();
DI即依賴注入,主要解決了2個問題:
除此以外,使用依賴注入還能夠帶來如下好處:
我記得以前在stackoverflow上看到過相似這樣的一個問題:
如何給5歲小孩解釋什麼叫DI?
得分最高的答案是:小孩在餓的時候只需喊一聲我要吃飯便可,而無需關注吃什麼,飯是怎麼來的等問題。
public class Kid { private readonly IFoodSupplier _foodSupplier; public Kid(IFoodSupplier foodSupplier) { _foodSupplier = foodSupplier; } public void HaveAMeal() { var food = _foodSupplier.GetFood(); //eat } }
DI的背後是一個DI Container(DI容器)在發揮做用。DI之因此可以工做須要兩個步驟:
組件註冊到DI容器中有3種方式:
.net平臺中的大多數DI框架都經過第三種方式進行組件註冊,爲了介紹這3種不一樣的註冊方式,咱們經過Java平臺下的Spring框架簡單介紹:Java中的Spring最先以XML文件的方式進行組件註冊,發展到目前主要經過Annotation來註冊。
假如咱們有CustomerRepository
接口和相應的實現CustomerRepositoryImpl
,下面用三種不一樣的方式將CustomerRepository
和CustomerRepositoryImpl
的對應關係註冊在DI容器中:
public interface CustomerRepository { List<Customer> findAll(); } public class CustomerRepositoryImpl implements CustomerRepository { public List<Customer> findAll() { List<Customer> customers = new ArrayList<Customer>(); Customer customer = new Customer("Bryan","Hansen"); customers.add(customer); return customers; } }
<bean name="customerRepository" class="com.thoughtworks.xml.repository.CustomerRepositoryImpl"/>
@Repository("customerRepository") public class CustomerRepositoryImpl implements CustomerRepository { public List<Customer> findAll() { //... } }
@Configuration public class AppConfig { @Bean(name = "customerRepository") public CustomerRepository getCustomerRepository() { return new CustomerRepositoryImpl(); } }
appContext.getBean("customerService", CustomerService.class);
一旦咱們將全部組件都註冊在容器中,就能夠靠DI容器進行依賴注入了。
正如上面Kid
的實現同樣,咱們經過構造器來注入IFoodSupplier
組件,這種方式也是依賴注入最佳方式。
public class Kid2 { public IFoodSupplier FoodSupplier { get; set; } public void HaveAMeal() { var food = FoodSupplier.GetFood(); //eat } }
即經過一個可讀寫的屬性完成注入,該方案的缺點在於爲了達到依賴注入的目的而破壞了對象的封裝性,因此不推薦。
經過添加方法的參數來完成注入,通常來講這種方式均可以經過構造器注入的方式來替換,因此也不太經常使用。值得一提的是asp.net core源碼中用到了這種注入方式。
下面描述了一個很簡單的Console application, 全部的組件都經過Castle Windsor容器進行構造器注入:
//register var container = new WindsorContainer(); container.Register(Component.For<IParser>().ImplementedBy<Parser>()); container.Register(Component.For<IWriter>().ImplementedBy<Writer>()); container.Register(Component.For<Application>()); //resolve var application = container.Resolve<Application>(); application.Execute("hel--lo, wor--ld"); //release container.Release(application);
這個例子向咱們展現了一個最簡單的依賴注入使用方式,register全部組件,resolve客戶端程序,最後的release步驟向咱們展現了若是顯示從DI容器獲得一個對象,應該顯示釋放該組件。這一步在大多數狀況下並非必須的,可是在特定場景下會發生內存泄漏。
下面的解決方案描述了一個典型的應用程序分層結構,該分層結構用來描述如何使用Catle windsor
進行依賴注入,注意:這並非一個合理的領域驅動案例,例如我將Domain
模型引用到了Application
或者ApplicationService
程序集中。
處在項目最底層的Repository
程序集定義了一組UserRepository
及其接口IUserRepository
,這樣的一個組件如何註冊在Windsor Container中呢?Castle提供了一種叫作WindsorInstaller
的機制:
public class RepositoryInstaller:IWindsorInstaller { public void Install(IWindsorContainer container, IConfigurationStore store) { container.Register(Component.For<IUserRepository>().ImplementedBy<UserRepository>().LifestyleScoped()); } }
該Installer利用Fluent Api定義了IUserRepository
和UserRepository
的對應關係,相對於Java中的Spring框架提供的代碼註冊方式,該方案的優越之處是顯而易見的。
另外的重點在於該Installer此時並無執行,只有當客戶端調用此Installer時,該組件才真真註冊進容器。這一點很關鍵,咱們後面還會提到。
接下來的ApplicationService
層使用了Repository
的抽象,一個典型的使用片段以下:
public class UserApplicationService : IUserApplicationService { private readonly IUserRepository _userRepository; public UserApplicationService(IUserRepository userRepository) { _userRepository = userRepository; } public void Register(User user) { _userRepository.Save(user); } //..... }
咱們經過構造器注入的方式注入了IUserRepository
,同時,做爲Service層,它也擁有本身的Installer:
public class ApplicationServiceInstaller:IWindsorInstaller { public void Install(IWindsorContainer container, IConfigurationStore store) { container.Register( Classes.FromThisAssembly().BasedOn<IApplicationService>().WithServiceDefaultInterfaces().LifestyleScoped()); } }
上面的例子示範瞭如何經過Castle提供的高級api來實現將該程序集中全部繼承於IApplicationService
的組件和其默認接口一次性所有註冊到DI容器中。
好比UserApplicationService
和IUserApplicationService
,以及將來將要實現的OrderApplicationService
以及IOrderApplicationService
。
接下來到客戶端程序集Application層,Application做爲使用ApplicationService
程序集的客戶端,他才擁有將組件註冊進DI容器的能力,咱們定義一個ApplicationBootstrap
來初始化DI容器並註冊組件:
public class ApplicationBootstrap { public static IWindsorContainer Container { get; private set; } public static IWindsorContainer RegisterComponents() { Container=new WindsorContainer(); Container.Install(FromAssembly.This()); Container.Install(FromAssembly.Containing<ApplicationServiceInstaller>()); Container.Install(FromAssembly.Containing<RepositoryInstaller>()); return Container; } }
注意Container.Install(...)
方法將執行不一樣應用程序的Installer,此時組件才真真註冊進DI容器。該實例展現瞭如何正確的使用依賴注入框架:
本文提供的源碼中所含的WebApplicationSample
項目演示瞭如何經過自定義WindsorControllerFactory
來實現mvc的依賴注入,經過自定義WindsorCompositionRoot
實現web api的依賴注入。
asp.net core實現了一個還算簡單的DI容器DenpendencyInjection,感興趣的同窗能夠閱讀其源碼。
本文所描述的案例提供下載,點擊下載