《.NET 相依性注入》電子書內容連載 (5)
續上集,介紹完幾個相關設計模式之後,接著要來看 DI 的模式,亦即注入物件的方式。
注入方式
DI 的核心概念是寬鬆耦合,是「針對介面寫程式」,故一旦開始在程式中運用 DI 技術,你可能會開始對「new 一個物件」的寫法更敏感。你可能會開始考慮,這個地方如果用 new 來建立特定實作類別的物件,將來需要修改程式時會不會很麻煩?如果只依賴介面或抽象類別會不會比較好?一旦開始出現這種現象,您已經朝向寬鬆耦合之路邁進了。
本節將介紹 DI 的三種注入方式,包括:
這三種注入方式基本上都符合下圖所描繪的模式。
建構式注入
建構式注入(Constructor Injection)指的是類別所需要的物件是由外界透過該類別的公開建構函式傳入。若需要注入 N 個相依物件,則建構函式通常至少要有 N 個引數,且引數的型別為介面或抽象類別。
已知應用例
.NET 基礎類別庫的 System.IO.Compression.ZipArchive 類別有一個建構函式提供了外界注入物件的機制,其函式原型如下:
public ZipArchive(Stream stream)
用法
假設類別 AuthenticationService 需要使用符合 IMessageService 介面的物件,可按以下步驟實現「建構式注入」:
範例程式
class AuthenticationService
{
private readonly IMessageService msgService;
public AuthenticationService(IMessageService service)
{
if (service == null)
{
throw new ArgumentException("service");
}
this.msgService = service;
}
}
程式說明:
顧名思義,屬性注入(Property Injection)就是透過物件的屬性來注入相依物件,另一個稱呼是「設定函式注入」(Setter Injection)。它與「建構式注入」相似,類別本身也需要有個成員變數來保存對相依物件的參考,但有個主要區別:「屬性注入」的時機比「建構式注入」來得晚,而且外界不一定會設定該屬性。也就是說,如欲採用「屬性注入」,類別本身通常要能夠取得相依物件的預設實作。如此一來,即使外界沒有注入相依物件,類別仍有預設的相依物件可用。
已知應用例
ASP.NET MVC 的 ControllerBuilder.SetControllerFactory 方法。此方法可用來切換 ASP.NET MVC 的 DefaultControllerFactory`(第 4 章有實作範例)。此方法之原型宣告如下:
public void SetControllerFactory(IControllerFactory controllerFactory)
用法
在類別中定義一個公開屬性,以便用戶端可以隨時設定該屬性來切換相依物件(或完全不設定)。若用戶端未曾設定該屬性,則由類別本身提供預設實作,或撰寫 null 檢查邏輯來避免執行時期發生 NullReferenceException。
範例程式
class AuthenticationService
{
private IMessageService msgService;
public IMessageService MessageService
{
get { return this.msgService; }
set { this.msgService = value; }
}
public void Login(string userId, string password)
{
MessageService.Send(...); // 使用相依物件。
}
}
屬性 MessageService 背後所使用的私有成員 msgService 不能宣告為 readonly,因為「屬性注入」允許外界於任何時候注入相依物件,甚至切換相依物件或根本不注入相依物件。對於不注入相依物件的情況,類別本身必須做些防護措施,以免其他地方存取相依物件時,因物件參考為 null 而引發 NullReferenceException 或其他異常。常見的防護措施是由類別本身提供預設的相依物件,時機可以選在建構函式中設定,或者更晚一點,在屬性的 getter 區塊中設定,參考以下程式範例:
public IMessageService MessageService
{
get
{
if (this.msgService == null)
{
this.msgService = new DefaultMessageService();
}
return this.msgService;
}
set { this.msgService = value; }
}
只是,如此一來,此類別又和特定實作 DefaultMessageService 綁在一起了。如果你覺得這種寫法不好,亦可考慮使用稍後介紹的 Method Injection(方法注入)或 Ambient Context(環境脈絡),或先前提過的 Factory 模式來解決,例如 Factory Method。
方法注入
「方法注入」(Method Injection)指的是用戶端每次呼叫某物件的方法時都必須透過方法的引數來傳入相依物件。此注入方式的適用時機如下:
已知應用例
ADO.NET 的 DbDataAdapter.Fill 方法有提供「方法注入」的機制,其函式原型如下:
protected virtual int Fill(
DataTable dataTable, // 查詢結果會填入此物件。
IDbCommand command, // 提供查詢命令的物件。
CommandBehavior behavior // 細部控制命令的行為。
)
用法
針對類別中的特定方法,把需要的相依物件加入方法的參數列。用戶端在每次呼叫這些方法時必須建立並傳入這些相依物件。
採用此注入方式時,除了傳入相依物件之外,經常會一併傳入其他相關的參數值,以便完成特定任務。例如:
public void ExecuteTask(ITaskContext context, int value1, int value2)
{
// 略
}
範例
仍以先前的 AuthenticateService 為例,如果 Login 方法允許外界指定使用何種驗證碼發送機制,便可改成這樣:
public void Login(string userId, string password, IMessageService service)
{
if (service == null)
{
throw new ArgumentException("service");
}
service.Send(...);
}
軟體框架和底層基礎建設(infrastructure)類型的函式庫也經常用到「方法注入」,因為這些類別庫通常不需要保存外界所提供的相依物件,而大多是針對每一次方法呼叫來讓多個物件共同達成任務。這同時也意味著,那些在方法呼叫之間傳遞的相依物件,通常也是在某高階模組需要完成特定工作時才臨時建立,而不會在應用程式的進入點或初始化階段就全都準備好這些物件。另一方面,如前兩節討論過的,提供「建構式注入」和「屬性注入」的類別則通常會在類別本身利用一個私有成員變數來保存外界注入的相依物件,而那些相依物件很有可能是高階模組初始化的時候、甚至在應用程式一開始執行時就已經預先建立的。
接著要介紹的是 Ambient Context 與 Service Locator 模式。
未完待續....
續上集,介紹完幾個相關設計模式之後,接著要來看 DI 的模式,亦即注入物件的方式。
注入方式
DI 的核心概念是寬鬆耦合,是「針對介面寫程式」,故一旦開始在程式中運用 DI 技術,你可能會開始對「new 一個物件」的寫法更敏感。你可能會開始考慮,這個地方如果用 new 來建立特定實作類別的物件,將來需要修改程式時會不會很麻煩?如果只依賴介面或抽象類別會不會比較好?一旦開始出現這種現象,您已經朝向寬鬆耦合之路邁進了。
本節將介紹 DI 的三種注入方式,包括:
- 建構式注入(Constructor Injection)
- 屬性注入(Property Injection)
- 方法注入(Method Injection)
這三種注入方式基本上都符合下圖所描繪的模式。
建構式注入
建構式注入(Constructor Injection)指的是類別所需要的物件是由外界透過該類別的公開建構函式傳入。若需要注入 N 個相依物件,則建構函式通常至少要有 N 個引數,且引數的型別為介面或抽象類別。
已知應用例
.NET 基礎類別庫的 System.IO.Compression.ZipArchive 類別有一個建構函式提供了外界注入物件的機制,其函式原型如下:
public ZipArchive(Stream stream)
用法
假設類別 AuthenticationService 需要使用符合 IMessageService 介面的物件,可按以下步驟實現「建構式注入」:
- 在 AuthenticationService 類別中宣告一個型別為 IMessageService 的成員變數。令此成員變數名稱為 msgService。
- 撰寫建構函式,在參數列中加入一個 IMessageService 型別的參數,然後將此參數值設定給成員變數 msgService。
範例程式
class AuthenticationService
{
private readonly IMessageService msgService;
public AuthenticationService(IMessageService service)
{
if (service == null)
{
throw new ArgumentException("service");
}
this.msgService = service;
}
}
程式說明:
- 在此範例中,AuthenticationService 提供了建構式注入的方式,讓外界得以傳入一個符合 IMessageService 介面的物件。若外界傳入不相容於此介面的型別,程式碼將無法通過編譯。
- 私有成員 msgService 的變數宣告之所以加上 readonly 關鍵字,是為了確保相依物件一旦在 AuthenticationService 的建構函式中設定完成後,就不能再改變。
- 建構函式還檢查了傳入的相依物件是否為 null,以確保一旦建構函式執行完畢,在此類別中的任何地方都可以使用相依物件,而無須擔心它是否為無效參考。
Note: 每當需要注入相依物件時,一般建議優先考慮「建構式注入」,因為其用法對呼叫端來說相當明確、直覺——建立物件時就要一併傳入所有相依物件,所以呼叫端透過建構函式便可得知某物件相依於哪些第三方元件。屬性注入
顧名思義,屬性注入(Property Injection)就是透過物件的屬性來注入相依物件,另一個稱呼是「設定函式注入」(Setter Injection)。它與「建構式注入」相似,類別本身也需要有個成員變數來保存對相依物件的參考,但有個主要區別:「屬性注入」的時機比「建構式注入」來得晚,而且外界不一定會設定該屬性。也就是說,如欲採用「屬性注入」,類別本身通常要能夠取得相依物件的預設實作。如此一來,即使外界沒有注入相依物件,類別仍有預設的相依物件可用。
已知應用例
ASP.NET MVC 的 ControllerBuilder.SetControllerFactory 方法。此方法可用來切換 ASP.NET MVC 的 DefaultControllerFactory`(第 4 章有實作範例)。此方法之原型宣告如下:
public void SetControllerFactory(IControllerFactory controllerFactory)
用法
在類別中定義一個公開屬性,以便用戶端可以隨時設定該屬性來切換相依物件(或完全不設定)。若用戶端未曾設定該屬性,則由類別本身提供預設實作,或撰寫 null 檢查邏輯來避免執行時期發生 NullReferenceException。
範例程式
class AuthenticationService
{
private IMessageService msgService;
public IMessageService MessageService
{
get { return this.msgService; }
set { this.msgService = value; }
}
public void Login(string userId, string password)
{
MessageService.Send(...); // 使用相依物件。
}
}
屬性 MessageService 背後所使用的私有成員 msgService 不能宣告為 readonly,因為「屬性注入」允許外界於任何時候注入相依物件,甚至切換相依物件或根本不注入相依物件。對於不注入相依物件的情況,類別本身必須做些防護措施,以免其他地方存取相依物件時,因物件參考為 null 而引發 NullReferenceException 或其他異常。常見的防護措施是由類別本身提供預設的相依物件,時機可以選在建構函式中設定,或者更晚一點,在屬性的 getter 區塊中設定,參考以下程式範例:
public IMessageService MessageService
{
get
{
if (this.msgService == null)
{
this.msgService = new DefaultMessageService();
}
return this.msgService;
}
set { this.msgService = value; }
}
只是,如此一來,此類別又和特定實作 DefaultMessageService 綁在一起了。如果你覺得這種寫法不好,亦可考慮使用稍後介紹的 Method Injection(方法注入)或 Ambient Context(環境脈絡),或先前提過的 Factory 模式來解決,例如 Factory Method。
看完「建構式注入」和「屬性注入」這兩個小節之後,如果您熟悉設計模式,會不會有一種感覺:DI 骨子裡根本就是 Strategy 模式嘛!底下附上 Strategy 模式的結構圖,讀者不妨把它當作一個練習,揣摩看看兩者的異同。
方法注入
「方法注入」(Method Injection)指的是用戶端每次呼叫某物件的方法時都必須透過方法的引數來傳入相依物件。此注入方式的適用時機如下:
- 提供服務的類別不需要在整個類別中使用特定相依物件,而只有在用戶端呼叫某些方法時才需要傳入那些物件。
- 用戶端每次呼叫特定方法時可能會傳入不同的物件,而這些物件唯一相同之處是它們都實作了同一個介面(或繼承自同一個抽象類別)。
已知應用例
ADO.NET 的 DbDataAdapter.Fill 方法有提供「方法注入」的機制,其函式原型如下:
protected virtual int Fill(
DataTable dataTable, // 查詢結果會填入此物件。
IDbCommand command, // 提供查詢命令的物件。
CommandBehavior behavior // 細部控制命令的行為。
)
用法
針對類別中的特定方法,把需要的相依物件加入方法的參數列。用戶端在每次呼叫這些方法時必須建立並傳入這些相依物件。
採用此注入方式時,除了傳入相依物件之外,經常會一併傳入其他相關的參數值,以便完成特定任務。例如:
public void ExecuteTask(ITaskContext context, int value1, int value2)
{
// 略
}
範例
仍以先前的 AuthenticateService 為例,如果 Login 方法允許外界指定使用何種驗證碼發送機制,便可改成這樣:
public void Login(string userId, string password, IMessageService service)
{
if (service == null)
{
throw new ArgumentException("service");
}
service.Send(...);
}
軟體框架和底層基礎建設(infrastructure)類型的函式庫也經常用到「方法注入」,因為這些類別庫通常不需要保存外界所提供的相依物件,而大多是針對每一次方法呼叫來讓多個物件共同達成任務。這同時也意味著,那些在方法呼叫之間傳遞的相依物件,通常也是在某高階模組需要完成特定工作時才臨時建立,而不會在應用程式的進入點或初始化階段就全都準備好這些物件。另一方面,如前兩節討論過的,提供「建構式注入」和「屬性注入」的類別則通常會在類別本身利用一個私有成員變數來保存外界注入的相依物件,而那些相依物件很有可能是高階模組初始化的時候、甚至在應用程式一開始執行時就已經預先建立的。
接著要介紹的是 Ambient Context 與 Service Locator 模式。
未完待續....
不用寫、不用管實作介面的concrete class,而只需要介面就可以完成程式的邏輯與測試,這爽度真的超高的。
回覆刪除沒有IoC,沒有透過介面來隔離類別的相依性,連測都沒法測。不過,實際上碰到的經驗,很多dev光卡在介面的建立,就望之卻步了。
順便分享一下,在老師這個範例中為了單元測試而將介面拉到建構式中,我在重構與導入單元測試時中有個特別的經驗,折衷的作法是直接把instance塞給private的field或property:
private ILogger logger = new myLogger();
而不透過public property或建構式來決定實作介面的instance。
好處是改動幅度小,可針對單支類別改動與撰寫測試,而且透過Visual Studio的Accessor,可以直接去mock private的field與property,進而做到單元測試中,可與外部類別隔離。
壞處是這是偷吃步的作法,只是為了測試而進行的重構,而非為了讓原本production code品質提升,降低類別耦合性,導致使用此類別的場景,無法透過public的介面,來自由抽換要使用的instance。
一點小經驗,藉著老師這主題,分享一下。
真的相當期待老師將這整系列的文章,整理成冊。
回覆刪除應該會是初階與中階SD最需要的一本書了。
Wow! 91 的回應真快! 此系列的這幾篇文章,到目前為止都還僅止於入門層次,我想對你來說應該是小菜一碟吧。如有謬誤,請多指正喔。
回覆刪除只能說對這系列文章相見恨晚啊。
回覆刪除雖然懂老師提的一些觀念,但從老師的精準用字遣詞與淺顯易懂的描述,還有太多太多值得我學習的地方 。
希望有朝一日也能跟老師一樣,是除了自己懂以外,還能讓別人懂
91 兄過獎了。一起努力 ^_^
回覆刪除這本剛發行的時候就拿到也讀過了,非常推薦。
回覆刪除在這本書出來之前,因為使用 Spring 作為 IoC 的主要舞台,花了不少時間讀了 Spring 的文件。
裡面的內容跟一些觀念,小提醒也都很棒。
Manning 這幾年的書,質量俱佳。遙想當年技術類書,首選 O'Reilly。
Manning Dependency Injection 這本書的確值得一讀。
的確,我也覺得這本書寫得不錯,也能適時填補目前台灣的程式設計書籍的技術區塊。所以每看一點,就整理一些筆記和自己的想法。
回覆刪除跟您的經驗雷同,我先前也比較常買歐萊禮的書,還有 Addison Wesley。
To IT Player:
回覆刪除我原先以為你講的書就是我目前在看的 DI in .NET (最近剛出版)。但剛剛突然想到,Manning 在 2009 年有出一本書:Dependency Injection。我猜你指的 DI 書籍應該是後者。
Hi Huanlin 您說對了,我看的是先出版的那一本。十月出版的這一本,其實 MEAP 的時候就有先看了。不管如何,這兩本都是質量兼具的書。您是國內有名的譯者(作者),有打算對這一本進行翻譯嗎?亦或是以您的觀點,出版中文書(紙本或是數位)也許都不錯。不過要擔心的是,這種議題的書,不太好賣就是了。雖然有樓上的 91 哥撰寫了大量的文章來推廣,不過要引起廣大共鳴真的不容易。anyway, 謝謝您願意花時間撰寫這類的文,我繼續長期潛水去 :)
回覆刪除多謝誇獎 :)
回覆刪除曾有翻譯本書的念頭,但這本書真的就如你所說,質量兼具,將近 600 頁,對翻譯來說,這"量"還真不少。而且...雖然我知道台灣仍有開發人員對此議題感興趣(例如您和 91 兄以及其他潛水的朋友),但市場還是太小,只能靠興趣支撐。這個系列的筆記,我自己也不知道能寫幾集。就隨緣吧! ^_^
如果真有翻譯書,替我們這些看原文書較吃力的潛水戶來說,真是一大福音呀!
回覆刪除畢竟在台灣推廣它或者讓身邊的同事朋友來接觸這個領域的東西,也相對容易。
^^
很感謝您的分享
阿德:
回覆刪除你的這篇留言提醒了我,原先要寫的筆記還沒寫完哩!得找時間再整理一下。
謝啦 ^^