Dependency Injection 筆記 (5)

.NET 相依性注入》電子書內容連載 (5)


本文摘自電子書《.NET 相依性注入》,您可至書籍首頁下載試閱章節。
書籍首頁網址:https://leanpub.com/dinet

上集,介紹完幾個相關設計模式之後,接著要來看 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 介面的物件,可按以下步驟實現「建構式注入」:
  1. 在 AuthenticationService 類別中宣告一個型別為 IMessageService 的成員變數。令此成員變數名稱為 msgService。
  2. 撰寫建構函式,在參數列中加入一個 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 模式。

未完待續....

Post Comments

技術提供:Blogger.