這篇距離上一篇 Unity 筆記,竟然已經兩年了...Orz

本文內容摘自《.NET 相依性注入》,主角是 Unity 的自動註冊(auto-registraction)功能。

共用的範例程式

為了避免往後重複太多相同的程式碼,這裡先列出共用的介面與類別。

假設情境

假設應用程式需要提供訊息通知機制,而此機制需支援多種發送管道,例如:電子郵件、
簡訊服務(Short Message Service)、行動應用程式的訊息推送(push notification)等等。簡單
起見,這裡僅實作其中兩種服務,而且發送訊息的部分都使用簡單的 Console.WriteLine()
來輸出訊息,方便觀察程式的執行結果。

設計

用一個 NotificationManager 類別來作為整個訊息通知功能的管理員。各類訊息通知機制則由以下類別提供:
  • EmailService:透過電子郵件發送訊息
  • SmsService:透過簡訊服務發送訊息

以上三個類別均實作同一個介面: IMessageService, 而且 NotificationManager 只知道

IMessageService 介面,而不直接依賴具象類別。以下類別圖描繪了它們的關係:

程式碼

訊息通知管理員的相關程式碼:

這裡採用了 Constructor Injection 的注入方式,由建構函式傳入訊息服務。其中的 Notify 方法則利用事先注入的訊息服務來發送訊息給指定的接收對象(引數 ‘to‘)。在真實世界中,你可能會需要用額外的類別來代表訊息接收對象(例如設計一個 MessageRecipient 類 別來封裝收件人的各項資訊),這裡為了示範而對這部分做了相當程度的簡化。

底下是各類訊息服務的程式碼:


自動註冊

「自動註冊」的另一種說法是「依慣例註冊」(registration by convention),主要用意是讓開發人員能夠少寫一些註冊型別的程式碼。Unity 是透過擴充方法 RegisterTypes(注意 “Types” 是複數)來提供自動註冊的功能。此方法有兩個多載版本,底下是它們的原型宣告:



先來看一個簡單的用法,使用的是上列標示(1) 的RegisterTypes 多載方法:

    // 範例:讓Unity 容器自動掃描組件並註冊型別。
var container = new UnityContainer();
container.RegisterTypes(
AllClasses.FromLoadedAssemblies(), // 掃描目前已經載入此應用程式的全部組件。
WithMappings.FromAllInterfaces); // 尋找所有介面。


這裡省略了大部分非必要的參數,而只傳入兩個參數:
  • 參數 types 是具象類別清單。這裡使用了 Unity 提供的輔助類別 AllClasses 的 FromLoadAssemblies 方法來提供類別清單。 
  • 參數 getFromTypes 是個用來取得來源型別清單的委派(delegate)。「來源型別」 指的就是註冊型別對應關係時的抽象型別。這裡傳入的委派是指向Unity 的 WithMappings.FromAllInterfaces 方法,作用是取得所有類別所實作的介面。

假設此範例應用程式目前已載入的組件當中已經有一個EmailService 類別,而且該類別實作了IMessageService 介面,那麼當應用程式需要取得符合IMessageService 介面的物件時,由於Unity 已經自動尋找並註冊了相關型別,於是我們便可透過先前提過的型別解析方法來取得物件:

    var svc = container.Resolve();


解決重複型別對應的問題

預設情況下,「自動註冊」會採用未具名的「預設註冊」(這名詞前面有提過),而且在碰到欲解析的類別有多載建構函式時,會根據一套既定規則來挑選建構函式。因此,如果只用剛才的簡單範例來進行自動註冊,很可能會在程式執行時發生型別註冊失敗或無法解析 特定型別的錯誤。

就拿前面提過的訊息通知服務的範例來說, 由於 EmailService 和 SmsService 都實作了 IMessageService 介面,若以上述範例程式來進行自動註冊,那麼當程式執行時,將會在呼叫 RegisterTypes 方法的地方發生重複型別對應的錯誤:
An unhandled exception of type ‘Microsoft.Practices.Unity.DuplicateTypeMappingException’ occurred in Microsoft.Practices.Unity.RegistrationByConvention.dll

碰到這種情形,你可以透過 RegisterTypes 方法的 getName 參數(它是個委派)來為不同的類別提供不同的註冊名稱。

    var container = new UnityContainer();
container.RegisterTypes(
AllClasses.FromLoadedAssemblies(), // 掃描目前已經載入此應用程式的全部組件。
WithMappings.FromAllInterfaces, // 尋找所有介面。
getName: WithName.TypeName); // 使用類別名稱來當作註冊名稱。


其中的WithName 是Unity 提供的輔助類別,而它的TypeName 方法可傳回指定型別的名稱。也就是說,如果沒有特別複雜的需求,我們可以直接把WithName.TypeName 方法傳遞給getName 參數。如此一來,每一個具象類別的型別對應關係就會以該類別的名稱來命名。於是,在解析物件的時候,也必須明白指定註冊名稱。像這樣:

    var svc = container.Resolve("EmailService");

另一個避免型別對應重複的解決方法,是利用參數overwriteExistingMappings。當你只需要找到任何一個可用的實作類別,而不在乎 Unity 最終會採用哪一個,此時便可將參數 overwriteExistingMappings 指定為 true,如此亦可避免型別對應重複的問題,而且無須使用具名註冊。參考以下範例:

    var container = new UnityContainer();
container.RegisterTypes(
AllClasses.FromLoadedAssemblies(), // 掃描目前已經載入此應用程式的全部組件。
WithMappings.FromAllInterfaces, // 尋找所有介面。
overwriteExistingMappings: true); // 有多個符合條件的類別時,採用最後找到的那個。

接著要來進一步了解 AllClasses 和WithMappings 類別還提供了哪些輔助功能。(略,請參考本書內容,或查看線上文件:AllClassesWithMappings

無恥連結:[書訊]《.NET 相依性注入》