緒上篇,這次的主題是延遲解析。
本文大綱:
小引
當我們說「解析某個型別/元件」時,意思通常是呼叫某類別的建構函式,以建立其執行個體(instance)。但有些場合,我們會希望解析時先不要生成物件,而是等到真正要呼叫物件的方法時才建立物件。這種延後建立物件的解析方式,叫做「延遲解析」(deferred resolution)。
延遲解析通常用在哪裡呢?一個典型的場合是欲解析的物件的創建過程需要花較多時間(例如解析時可能因為建構函式需要注入其他物件,而產生多層巢狀解析的情形),而我們希望能加快這個過程,以提升應用程式的回應速度。
本節提供兩種實現延遲解析的作法,一種是 Lazy<T>,另一種是「自動工廠」(automatic factories)。以下兩個小節將分別介紹這兩種作法。
註:本文使用的 IMessageService 相關類別的程式碼在另一篇筆記:Unity 入門 (6) - 自動註冊。
使用 Lazy<T>
Unity 可以讓我們直接使用 .NET Framework 內建的 Lazy<T> 來實現延遲解析。試比較底下兩個範例,首先是一般的寫法:
然後是 Lazy<T> 的延遲解析寫法,由於註冊型別的程式碼不變,故只列出解析的部分:
「自動工廠」(automatic factories)指的是 DI 容器能夠自動生成一個輕量級的工廠類別,這樣我們就不用花工夫自己寫了。那麼,什麼情況會用到自動工廠呢?通常是用來實現「延遲解析」,或者應付更複雜、更動態的晚期繫結(late binding)的需求。
仍舊以先前提過的 NotificationManager 和 IMessageService 為例。假設 EmailService 這個元件在建立執行個體時需要花費較長的時間(約五秒),參考以下程式片段:
如果照一般的元件解析方式,會這麼寫:
這種寫法,在其中的「(2) 解析」這個步驟會發生下列動作:
也就是說,在解析 NotificationManager 時便一併建立了 EmailService 物件,故此步驟至少要花五秒的時間才能完成。然而,現在我們想要延後相依物件的創建時機,亦即等到真正呼叫元件的方法時,才真正建立其相依物件的執行個體。像這種場合,我們可以利用 Func<T> 與 Unity 的「自動工廠」來達到延遲解析相依物件的效果。作法很簡單,只要修改 NotificationManager 類別就行了。如下所示:
另一方面,原先的「註冊、解析、呼叫」三步驟的程式碼都不用任何改變。方便閱讀起見,這裡再將註冊元件的程式碼貼上來:
請注意,NotificationManager 的建構函式要求注入的明明是 Func<IMessageService>,可是向容器註冊元件時,卻依舊寫 IMessageService,而不用改成 Func<IMessageService>(要五毛,給一塊)。如此一來,當你想要為既有程式碼加入延遲解析(延遲建立相依物件)的能力時,就可以少改一些程式碼。這是 Unity 容器的「自動工廠」提供的好處。
進一步解釋,當 Unity 容器欲解析 NotificationManager 時,發現其建構函式需要一個 Func<IMessageService> 委派(delegate),於是便自動幫你生成這個物件,並將它注入至 NotificationManager 類別的建構函式。由於注入的是委派物件(你可以把它當作是個**工廠方法**),故此時並沒有真正建立 IMessageService 物件,而是等到上層模組呼叫此元件的 Notify 方法時,才透過呼叫委派方法來建立 IMessageService 物件 。
當然,Unity 容器的「自動工廠」可能無法滿足某些需求。比如說,有些相依物件的創建邏輯比較複雜,需要你撰寫自訂的物件工廠。這個時候,你可能會想要知道如何注入自訂工廠。
注入自訂工廠
當你想要讓 Unity 容器在解析特定元件時使用你的自訂工廠來建立所需之相依物件,Unity 框架的 InjectionFactory 類別可以派上用場。
延續上一個小節的 NotificationManager 範例。現在假設你寫了一個物件工廠來封裝 IMessageService 的創建邏輯,像這樣:
此物件工廠的 GetService 方法會根據執行時期的某些變數來決定要返回 EmailService 還是 SmsService 的執行個體。EmailService 與 SmsService 這兩個類別都實作了 IMessageService 介面,它們的程式碼在這裡並不重要,故未列出。如需查看這些類別的程式碼,可參閱稍早的〈共用的範例程式〉一節的內容。
NotificationManager 的建構函式與上一節的範例相同,仍舊是注入 Func<IMessageService>。如下所示:
剩下的工作,就是告訴 Unity 容器:「在需要解析 IMessageService 的時候,請使用我的 MessageServiceFactory 來建立物件。」參考以下程式片段:
註冊元件的部分需要加以說明,如下:
此範例所使用的 RegisterType 是個擴充方法,其原型宣告如下:
InjectionFactory 類別繼承自 InjectionMember,而此範例所使用的建構函式之原型宣告為:
摘自:《 .NET 相依性注入》第 7 章。
本文大綱:
- 小引
- 使用 Lazy<T>
- 使用自動工廠
- 注入自訂工廠
小引
當我們說「解析某個型別/元件」時,意思通常是呼叫某類別的建構函式,以建立其執行個體(instance)。但有些場合,我們會希望解析時先不要生成物件,而是等到真正要呼叫物件的方法時才建立物件。這種延後建立物件的解析方式,叫做「延遲解析」(deferred resolution)。
延遲解析通常用在哪裡呢?一個典型的場合是欲解析的物件的創建過程需要花較多時間(例如解析時可能因為建構函式需要注入其他物件,而產生多層巢狀解析的情形),而我們希望能加快這個過程,以提升應用程式的回應速度。
本節提供兩種實現延遲解析的作法,一種是 Lazy<T>,另一種是「自動工廠」(automatic factories)。以下兩個小節將分別介紹這兩種作法。
註:本文使用的 IMessageService 相關類別的程式碼在另一篇筆記:Unity 入門 (6) - 自動註冊。
使用 Lazy<T>
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) 解析」這個步驟會發生下列動作:
- DI 容器欲解析 NotificationManager,發現其建構函式需要傳入 IMessageService 物件,於是先解析 IMessageService。
- 由於先前向容器註冊元件時已經指定由 EmailService 來作為 IMessageService 的實作類別,故容器會先建立一個 EmailService 物件,然後將此物件傳入 NotificationManager 的建構函式,以便建立一個 NotificationManager 物件。
也就是說,在解析 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>();
註冊元件的部分需要加以說明,如下:
- 先建立一個 Func<IMessageService> 的委派物件,讓它指向 MessageServiceFactory 物件的 GetService 方法。
- 接著呼叫 Unity 容器的 RegisterType 方法,告訴容器:解析 IMessageService 時,請用我提供的自訂工廠的 GetService 方法,而這個工廠方法已經包在剛才建立的委派物件(變數 factoryMethod),並透過 Unity 的 InjectionFactory 將此工廠方法再包一層,以便保存於 Unity 容器。
此範例所使用的 RegisterType 是個擴充方法,其原型宣告如下:
public static IUnityContainer RegisterType<T>(this IUnityContainer container, params InjectionMember[] injectionMembers);
InjectionFactory 類別繼承自 InjectionMember,而此範例所使用的建構函式之原型宣告為:
public InjectionFactory(Func<IUnityContainer, object> factoryFunc);
註:如需 InjectionFactory 類別的詳細說明,可參考線上文件。
摘自:《 .NET 相依性注入》第 7 章。
沒有留言: