一、DIP原則
- 高層模塊不應該依賴於底層模塊,二者都應該依賴於抽象。
- 抽象不應該依賴於細節,細節應該依賴於抽象。
該原則理解起來稍微有點抽象,我們可以將該原則通俗的理解為:"依賴於抽象”。
該規則告訴我們,程序中所有的依賴關系都應該終止於抽象類或者接口
,從而達到松耦合的目的。因為我們在應用程序中編寫的大多數具體類都是不穩定的
。我們不想直接依賴於這些不穩定的具體類。通過把它們隱藏在抽象和接口
的后面,可以隔離它們的不穩定性。
舉個例子
一個Button對象會觸發Click方法,當被按下時,會調用Light對象的TurnOn方法,否則會調用Light對象的TurnOff方法。

這個設計存在兩個問題:
- Button類直接依賴於Light類,這種依賴關系意味着當Light改變時,Button類會受到影響;
- Button對象只能控制Light對象,想要控制電視或者冰箱就不行了;
新的設計:

這個方案對那些需要被Button控制的對象提出了一個約束。需要被Button控制的對象必須要實現ISwitchableDevice接口。
所為原則
,只是描述了什么是對的,但是並沒有說清楚如何去做。在軟件工程中,我們經常使用DI(依賴注入)
來達到這個目的。但是提到依賴注入,人們又會經常提起IoC
這個術語。所以先讓我們來了解下什么是IoC。
二、IoC
IoC的全名是Inverse of Control,即控制反轉。這一術語並不是用來描述面向對象的某種原則或者模式,IoC體現為一種流程控制的反轉,一般用來對框架進行設計。
舉個例子
ReportService是一個用來顯示報表的流程,該流程包括Trim()
,Clean()
,Show()
三個環節。
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();
這樣的一個設計體現了過程式的思考方式,客戶端依次調用每個環節從而組成了整個報表顯示流程,這樣的代碼體現了:客戶端擁有流程控制權。
我們來分析下這段代碼,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(Dependency Injection)
DI即依賴注入,主要解決了2個問題:
- 松耦合,由DI容器來創建對象,符合DIP原則;
- 符合IoC的思想,整個應用程序事先定義好了一套可工作的流程,通過在客戶端替換DI容器中的具體實現達到重寫某個組件的目的;
除此之外,使用依賴注入還可以帶來以下好處:
- 促使你寫出更加符合面向對象原則的代碼,符合優先使用對象組合,而不是繼承的原則;
- 使系統更加具有可測試性;
- 使系統更加具備可擴展性和可維護性;
- 由於所有組件都由DI容器管理,所以可以很方便的實現AOP攔截
我記得之前在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容器中;
- DI容器統一管理所有依賴關系,將依賴組件注入到所需要相應的組件中;
3.1 組件的注冊方式
組件注冊到DI容器中有3種方式:
- 通過XML文件注冊
- 通過Attribute(Annotation)注冊
- 通過DI容器提供的API注冊
.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;
}
}
3.1.1、xml文件注冊
<bean name="customerRepository" class="com.thoughtworks.xml.repository.CustomerRepositoryImpl"/>
3.1.2、Annotation注冊
@Repository("customerRepository")
public class CustomerRepositoryImpl implements CustomerRepository {
public List<Customer> findAll() {
//...
}
}
3.1.3、通過Java代碼來實現注冊
@Configuration
public class AppConfig {
@Bean(name = "customerRepository")
public CustomerRepository getCustomerRepository() {
return new CustomerRepositoryImpl();
}
}
3.1.4通過下面的方式從Container來獲取一個實例
appContext.getBean("customerService", CustomerService.class);
一旦我們將所有組件都注冊在容器中,就可以靠DI容器進行依賴注入了。
3.2 依賴注入的三種方式
3.2.1. 構造器注入
正如上面Kid
的實現一樣,我們通過構造器來注入IFoodSupplier
組件,這種方式也是依賴注入最佳方式。
3.2.2. 屬性注入
public class Kid2
{
public IFoodSupplier FoodSupplier { get; set; }
public void HaveAMeal()
{
var food = FoodSupplier.GetFood();
//eat
}
}
即通過一個可讀寫的屬性完成注入,該方案的缺點在於為了達到依賴注入的目的而破壞了對象的封裝性,所以不推薦。
3.2.3 方法注入
通過添加方法的參數來完成注入,一般來說這種方式都可以通過構造器注入的方式來替換,所以也不太常用。值得一提的是asp.net core源碼中用到了這種注入方式。
四、依賴注入實例
1、Register Resolve Release Pattern
下面描述了一個很簡單的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容器得到一個對象,應該顯示釋放該組件。這一步在大多數情況下並不是必須的,但是在特定場景下會發生內存泄漏。
2、.net平台下依賴注入最佳實踐
下面的解決方案描述了一個典型的應用程序分層結構,該分層結構用來描述如何使用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容器。該實例展示了如何正確的使用依賴注入框架:
- 不同的程序集之間通過接口依賴,符合DIP原則;
- 通過依賴注入的方式定義好了可運行的流程,但是客戶端可以注冊不同的組件到DI容器中,符合IoC的思想;
3、如何在asp.net mvc和asp.net webapi使用依賴注入
本文提供的源碼中所含的WebApplicationSample
項目演示了如何通過自定義WindsorControllerFactory
來實現mvc的依賴注入,通過自定義WindsorCompositionRoot
實現web api的依賴注入。
五、高級進階
asp.net core實現了一個還算簡單的DI容器DenpendencyInjection,感興趣的同學可以閱讀其源碼。
六、源碼下載
本文所描述的案例提供下載,點擊下載