當我們說「解析某個型別/組件」時,意思通常是呼叫某類別的建構函式,以建立其實例(instance)。但有些場合,我們會希望解析時先不要生成對象,而是等到真正要呼叫對象的方法時才建立對象。這種延後建立對象的解析方式,叫做「延遲解析」(deferred resolution)。
延遲解析通常用在哪裡呢?一個典型的場合是欲解析的對象的創建過程需要花較多時間(例如解析時可能因為建構函式需要注入其他對象,而產生多層巢狀解析的情形),而我們希望能加快這個過程,以提升應用程序的響應速度。
本文介紹兩種實現延遲解析的作法,一種是 Lazy<T>,另一種是「自動工廠」(automatic factories)。
為了避免往後重復太多相同的程序代碼,這裡先列出共享的接口與類別。
假設情境
假設應用程序需要提供訊息通知機制,而此機制需支持多種發送管道,例如:電子郵件、簡訊服務(Short Message Service)、行動應用程序的訊息推送(push notification)等等。簡單起見,這裡僅實作其中兩種服務,而且發送訊息的部分都使用簡單的 Console.WriteLine() 來輸出訊息,方便觀察程序的執行結果。
設計
用一個 NotificationManager 類來作為整個訊息通知功能的管理員。各類訊息通知機制則由以下兩個類提供:
以上三個類均實作同一個接口: IMessageService, 而且 NotificationManager 只知道 IMessageService 接口,而不直接依賴具象類。下圖描繪了它們的關系:

代碼
訊息通知管理員的相關代碼:
public interface IMessageService
{
void SendMessage(string to, string msg);
}
public class EmailService : IMessageService
{
public void SendMessage(string to, string msg)
{
Console.WriteLine(" 透過 EmailService 發送郵件給 {0}。", to);
}
}
public class SmsService : IMessageService
{
public void SendMessage(string to, string msg)
{
Console.WriteLine(" 透過 SmsService 發送簡訊給 {0}。", to);
}
}
這裡采用了 Constructor Injection 的注入方式,由建構函式傳入訊息服務。其中的 Notify 方法則利用事先注入的訊息服務來發送訊息給指定的接收對象(自變量 "to")。在真實世界中,你可能會需要用額外的類別來代表訊息接收對象(例如設計一個 MessageRecipient 類 別來封裝收件人的各項信息),這裡為了示范而對這部分做了相當程度的簡化。
底下是各類訊息服務的程序代碼:
public interface IMessageService
{
void SendMessage(string to, string msg);
}
public class EmailService : IMessageService
{
public void SendMessage(string to, string msg)
{
Console.WriteLine(" 透過 EmailService 發送郵件給 {0}。", to);
}
}
public class SmsService : IMessageService
{
public void SendMessage(string to, string msg)
{
Console.WriteLine(" 透過 SmsService 發送簡訊給 {0}。", to);
}
}
Unity 可以讓我們直接使用 .NET Framework 內建的 Lazy<T> 來實現延遲解析。試比較底下兩個范例,首先是一般的寫法:
// 一般寫法
var container = new UnityContainer();
container.RegisterType<IMessageService, EmailService>(); // 注冊
var svc = container.Resolve<IMessageService>(); // 解析組件(呼叫實作類別的建構函式)
svc.SendMessage("Michael", "Hello!"); // 使用組件
然後是 Lazy<T> 的延遲解析寫法,由於注冊型別的程序代碼不變,故只列出解析的部分:
var lazyObj = container.Resolve<Lazy<IMessageService>>(); // 延遲解析
var svc = lazyObj.Value; // 此時才真正呼叫類別的建構函式
svc.SendMessage("Michael", "Hello!"); // 使用組件
注:有關 Lazy&T& 的用法,請參閱 MSDN 在線文件,或搜尋關鍵詞「Lazy of T」。
「自動工廠」(automatic factories)指的是 DI 容器能夠自動生成一個輕量級的工廠類別,這樣我們就不用花工夫自己寫了。那麼,什麼情況會用到自動工廠呢?通常是用來實現「延遲解析」,或者應付更復雜、更動態的晚期綁定(late binding)的需求。
仍舊以先前提過的 NotificationManager 和 IMessageService 為例。假設 EmailService 這個組件在建立實例時需要花費較長的時間(約五秒),參考以下程序片段:
public class EmailService : IMessageService
{
public EmailService()
{
// 以暫停線程的方式來仿真對象生成的過程需要花費較長時間。
System.Threading.Thread.Sleep(5000);
}
// 其余程序代碼在這裡並不重要,予以省略。
}
如果照一般的組件解析方式,會這麼寫:
// (1) 注冊
var container = new UnityContainer();
container.RegisterType<IMessageService, EmailService>();
// (2) 解析
var notySvc = container.Resolve<NotificationManager>();
// (3) 呼叫
notySvc.Notify("Michael", "自動工廠范例");
這種寫法,在其中的「(2) 解析」這個步驟會發生下列動作:
也就是說,在解析 NotificationManager 時便一並建立了 EmailService 對象,故此步驟至少要花五秒的時間才能完成。然而,現在我們想要延後相依對象的創建時機,亦即等到真正呼叫組件的方法時,才真正建立其相依對象的實例。像這種場合,我們可以利用 Func<T> 與 Unity 的「自動工廠」來達到延遲解析相依對象的效果。作法很簡單,只要修改 NotificationManager 類別就行了。如下所示:
class NotificationManager
{
private IMessageService _msgService;
private Func<IMessageService> _msgServiceFactory
public NotificationManager(Func<IMessageService> svcFactory)
{
// 把工廠方法保存在委派對象裡
_msgServiceFactory = svcFactory;
}
public void Notify(string to, string msg)
{
// 由於每次呼叫 _msgServiceFactory() 時都會建立一個新的 IMessageService 對象,
// 這裡用一個私有成員變量來保存先前建立的對象,以免不斷建立新的實例。
// 當然這並非必要;有些場合,你可能會想要每次都建立新的相依對象。
if (_msgService == null)
{
_msgService = _msgServiceFactory();
}
_msgService.SendMessage(to, msg);
}
}
另一方面,原先的「注冊、解析、呼叫」三步驟的程序代碼都不用任何改變。方便閱讀起見,這裡再將注冊組件的程序代碼貼上來:
// (1) 注冊 var container = new UnityContainer(); container.RegisterType<IMessageService, EmailService>();
請注意,NotificationManager 的建構函式要求注入的明明是 Func<IMessageService>,可是向容器注冊組件時,卻依舊寫 IMessageService,而不用改成 Func<IMessageService>(要五毛,給一塊)。如此一來,當你想要為既有程序代碼加入延遲解析(延遲建立相依對象)的能力時,就可以少改一些程序代碼。這是 Unity 容器的「自動工廠」提供的好處。
進一步解釋,當 Unity 容器欲解析 NotificationManager 時,發現其建構函式需要一個 Func<IMessageService> 委派(delegate),於是便自動幫你生成這個對象,並將它注入至 NotificationManager 類別的建構函式。由於注入的是委派對象(你可以把它當作是個工廠方法),故此時並沒有真正建立 IMessageService 對象,而是等到上層模塊呼叫此組件的 Notify 方法時,才透過呼叫委派方法來建立 IMessageService 對象 。
當然,Unity 容器的「自動工廠」可能無法滿足某些需求。比如說,有些相依對象的創建邏輯比較復雜,需要你撰寫自定義的對象工廠。這個時候,你可能會想要知道如何注入自定義工廠。
當你想要讓 Unity 容器在解析特定組件時使用你的自定義工廠來建立所需之相依對象,Unity 框架的 InjectionFactory 類別可以派上用場。
延續上一個小節的 NotificationManager 范例。現在假設你寫了一個對象工廠來封裝 IMessageService 的創建邏輯,像這樣:
class MessageServiceFactory
{
public IMessageService GetService()
{
bool isEmail = CheckIfEmailIsUsed();
if (isEmail)
{
return new EmailService();
}
else
{
return new SmsService();
}
}
}
此對象工廠的 GetService 方法會根據執行時期的某些變量來決定要返回 EmailService 還是 SmsService 的實例。EmailService 與 SmsService 這兩個類別都實作了 IMessageService 接口,它們的程序代碼在這裡並不重要,故未列出。如需查看這些類別的程序代碼,可參閱稍早的〈共享的范例程序〉一節的內容。
NotificationManager 的建構函式與上一節的范例相同,仍舊是注入 Func<IMessageService>。如下所示:
class NotificationManager
{
private IMessageService _msgService;
private Func<IMessageService> _msgServiceFactory
public NotificationManager(Func<IMessageService> svcFactory)
{
// 把工廠方法保存在委派對象裡
_msgServiceFactory = svcFactory;
}
// (已省略其他不重要的程序代碼)
}
剩下的工作,就是告訴 Unity 容器:「在需要解析 IMessageService 的時候,請使用我的 MessageServiceFactory 來建立對象。」參考以下程序片段:
var container = new UnityContainer(); // 注冊 Func<IMessageService> factoryMethod = new MessageServiceFactory().GetService; container.RegisterType<IMessageService>(new InjectionFactory(c => factoryMethod())); // 解析 container.Resolve<NotificationManager>();
注冊組件的部分需要加以說明,如下:
此范例所使用的 RegisterType 是個擴充方法,其原型宣告如下:
public static IUnityContainer RegisterType<T>(this IUnityContainer container,
params InjectionMember[] injectionMembers);
InjectionFactory 類別繼承自 InjectionMember,而此范例所使用的建構函式之原型宣告為:
public InjectionFactory(Func<IUnityContainer, object> factoryFunc);
注:如需 InjectionFactory 類別的詳細說明,可參考在線文件。
本文摘自:《 .NET 依賴注入 》第 7 章。