我們在這個系列的前四篇文章中分別介紹了SOLID原則中的前四個原則,今天來介紹最后一個原則——依賴注入原則。依賴注入(DI)是一個很簡單的概念,實現起來也很簡單。但是簡單卻掩蓋不了它的重要性,如果沒有依賴注入,前面的介紹的SOLID技術原則都不可能實際應用。
控制反轉(IoC)
人們在談論依賴注入的時候,經常也會談到另一個概念——控制反轉(IoC)。按照大內老A的解釋:“IoC主要體現了這樣一種設計思想:通過將一組通用流程的控制權從應用轉移到框架中以實現對流程的復用,並按照“好萊塢法則”實現應用程序的代碼與框架之間的交互“。概念比較抽象,我們拆開解讀一下。
我們要做的任何一件事情,無論大小,都可以分解為相應的步驟。所以任何一件事情都有其固定的流程。與現實問題域一樣,解決方案域(程序實現)也是這樣。所以IoC控制可以理解為“對流程的控制”。以HTTP請求處理的流程為例,在傳統面向類庫編程的時代,針對HTTP請求處理的流程牢牢控制在應用程序手中。在引入框架之后,請求處理的控制權轉移到了框架手上。類庫(Library)和框架(Framework)的不同之處在於,前者往往只是提供實現某種單一功能的API,而后者則針對一個目標任務對這些單一功能進行編排形成一個完整的流程,這個流程在一個引擎的驅動下自動執行。如此,所有使用此框架的程序都可以復用關於HTTP請求處理的流程。
在好萊塢,把簡歷遞交給演藝公司后就只有回家等待。由演藝公司對整個娛樂項目的完全控制,演員只能被動式的接受電影公司的工作,在需要的環節中,完成自己的演出。“不要給我們打電話,我們會給你打電話(don‘t call us, we‘ll call you)”這是著名的好萊塢法則。
IoC完美地體現了這一法則,對於ASP.NET MVC應用開發來說,我們只需要按照約定規則(比如目錄結構和命名等)定義相應的Controller類型和View文件就可以了,這就是所謂的“約定大於配置”。當ASP.NET MVC框架在進行處理請求的過程中,它會根據解析生成的路由參數定義為對應的Controller類型,並按照預定義的規則找到我們定義的Controller,然后自動創建並執行它。如果定義在當前Action方法需要呈現一個View,框架自身會根據預定義的目錄約定找到我們定義的View文件,並對它實施動態編譯和執行。整個流程處處體現了“框架Call應用”的好萊塢法則。
簡單的說,控制反轉(IoC)的過程就是一組通用流程的控制權從應用程序轉移到框架中的過程,為的是實現流程的復用。但是有一個問題,被反轉的僅僅是一個泛化的流程,在特定場景可能會有一些特殊的流程或者流程節點,此時就需要進行流程定制。定制一般是通過框架預留的擴展點進行的,比如ASP.NET中的HttpHandler和HttpModule,ASP.NET Core中的Middleware。
前面提到控制反轉(IoC)是一種設計思想。所以控制反轉(IoC)並不能解決某一類具體的問題。但是基於控制反轉(IoC)思想的設計模式卻可以,最簡單直觀的就是模板方法模式。該模式主張將一個可復用的工作流程或者由多個步驟組成的算法定義成模板方法,組成這個流程或者算法的步驟實現在相應的虛方法之中,模板方法根據按照預先編排的流程去調用這些虛方法。所有這些方法均定義在同一個類中,我們可以通過派生該類並重寫相應的虛方法達到對流程定制的目的。
public class TemplateMethod
{
//流程編排
public void ABCD()
{
A();
B();
C();
D();
}
//步驟A
protected virtual void A() { }
//步驟B
protected virtual void B() { }
//步驟C
protected virtual void C() { }
//步驟D
protected virtual void D() { }
}
依賴注入(DI)
依賴注入(DI)也是架構在控制反轉思想上的一種模式。在這里我們將提供的對象統稱為“服務”、“服務對象”或者“服務實例”。在一個采用DI的應用中,在定義某個服務類型的時候,我們直接將依賴的服務采用相應的方式注入進來。按照“面向接口編程”的原則,被注入的最好是依賴服務的接口而非實現。正確的依賴注入對於項目的絕大多數代碼都是不可見的,它們(注冊代碼)被局限在一個很小的代碼范圍內,通常是一個獨立的程序集。
在應用啟動的時候,會對所需的服務進行全局注冊。服務一般都是針對接口進行注冊的,服務注冊信息的核心目的是為了在后續消費過程中能夠根據接口創建或者提供對應的服務實例。按照“好萊塢法則”,應用只需要定義好所需的服務,服務實例的激活和調用則完全交給框架來完成,而框架則會采用一個獨立的“容器(Container)”來提供所需的每一個服務實例。我們將這個被框架用來提供服務的容器稱為“DI容器”,也由很多人將其稱為“IoC容器”。所有的DI容器都符合注冊、解析、釋放模式。
依賴注入的三種注入方式
1.構造函數注入
public class TaskService
{
private ITaskOneRepository taskOneRepository;
private ITaskTwoRepository taskTwoRepository;
public TaskService(
ITaskOneRepository taskOneRepository,
ITaskTwoRepository taskTwoRepository)
{
this.taskOneRepository = taskOneRepository;
this.taskTwoRepository = taskTwoRepository;
}
}
優點:
- 在構造方法中體現出對其他類的依賴,一眼就能看出這個類需要其他那些類才能工作。
- 脫離了IOC框架,這個類仍然可以工作(窮人的依賴注入)。
- 一旦對象初始化成功了,這個對象的狀態肯定是正確的。
缺點:
- 構造函數會有很多參數。
- 有些類是需要默認構造函數的,比如MVC框架的Controller類,一旦使用構造函數注入,就無法使用默認構造函數。
2.屬性注入
public class TaskService
{
private ITaskRepository taskRepository;
private ISettings settings;
public TaskService(
ITaskRepository taskRepository,
ISettings settings)
{
this.taskRepository = taskRepository;
this.settings = settings;
}
public void OnLoad()
{
taskRepository.settings = settings;
}
}
優點:
- 在對象的整個生命周期內,可以隨時動態的改變依賴。
- 非常靈活。
缺點:
- 對象在創建后,被設置依賴對象之前這段時間狀態是不對的(從構造函數注入的依賴實例在類的整個生命周期內都可以使用,而從屬性注入的依賴實例還能從類生命周期的某個中間點開始起作用)。
- 不直觀,無法清晰地表示哪些屬性是必須的。
3.方法注入
public class TaskRepository
{
private ISettings settings;
public void PrePare(ISettings settings)
{
this.settings = settings;
}
}
優點:
- 比較靈活。
缺點:
- 新加入依賴時會破壞原有的方法簽名,如果這個方法已經被其他很多模塊用到就很麻煩。
- 與構造方法注入一樣,會有很多參數。
在這三種注入方式中,推薦使用構造函數注入。最重要的原因是服務應該是獨立自治的,即使脫離了DI框架,這個服務應該仍然可以工作。構造函數注入就符合這一要求,即使脫離了DI框架,仍然可以手動注入依賴的服務。
依賴注入反模式 —— Service Locator
假設我們需要定義一個服務類型C,它依賴於另外兩個服務A和B,后者對應的服務接口分別為IA和IB。如果當前應用中具有一個DI容器(Container),那么我們可以采用如下兩種方式來定義這個服務類型C。
public class C : IC
{
public IA A { get; }
public IB B { get; }
public C(IA a, IB b)
{
A = a;
B = b;
}
public void Invoke()
{
a.Invoke();
b.Invoke();
}
}
public class C : IC
{
public Container Container { get; }
public C(Container container)
{
Container = container;
}
public void Invoke()
{
Container.GetService<IA>().Invoke();
Container.GetService<IB>().Invoke();
}
}
從表面上看,這兩種方式並沒有什么太大的區別。都解決了針對依賴服務的耦合問題,將針對服務實現依賴變成針對接口的依賴。但是,其實后一種方式並不是依賴注入模式,而是服務定位器反模式。因為看起來和依賴注入模式很相似,人們經常會忽視它給代碼帶來的破壞。
我們可以從“DI容器”和“Service Locator”被誰使用的角度來區分這兩種設計模式的差別。DI容器的使用者是框架而不是應用程序,Service Locator的使用者是應用程序,應用程序利用它來提供服務實例。有時候,它是唯一能提供依賴注入鈎子的方式。
那么Service Locator(服務定位器反模式)對代碼造成了哪些破壞呢?
- 因為容器中的服務是全局注冊的,所以DI容器是靜態的,這會導致出現靜態類或者服務中出現靜態變量和字段。
- 服務定位器暴露了容器存在的信息。原因是服務定位器允許類檢索任何對象,無論是否合適。這樣違背了依賴注入的“好萊塢准則”,不要調用我們,我們會調用你。
- 服務定位器會直接委托Container實例來解析實例對象,這樣會造成服務沒有依賴的假象。但是服務肯定是有依賴的,不然為什么要從服務定位器獲取它們呢。
雖然我們對服務定位器反模式提出了這么多批判,但是它還是非常常見。因為有時候根本沒有從構造函數注入的任何機會,唯一的選擇就是服務定位器。畢竟它肯定比不注入依賴要好,也比手動構造注入依賴要好。
總結
依賴注入(DI)是架構在控制反轉(IoC)思想上的一種模式,所有的DI容器都符合注冊、解析、釋放模式。注入代碼通常在一個獨立的程序集,注入的最好是依賴服務的接口而非實現,服務實例的激活和調用則完全交給框架來完成。在依賴注入的三種注入方式中,推薦使用構造函數注入。另外在沒有從構造函數注入的機會時,可以考慮選擇服務定位器反模式。選擇模式的原則是:依賴注入模式優於服務定位器反模式,優於手動構造注入依賴,優於不注入依賴。
參考
《C#敏捷開發實踐》