《.NET 相依性注入》電子書內容連載 (6)
續上集,接著要談 Ambient Context 與 Service Locator 模式。
Ambient Context 模式
前述三種注入相依物件的方式,有些場合可能不適用,例如:應用程式特定執行環境的範圍內需要共享特定物件。碰到這種場合,便可以考慮採用 **Ambient Context **(環境脈絡)模式來解決。
Ambient Context又叫做 Context Object(環境物件),是一種常見的設計模式,主要用於跨階層、跨模組共享物件、界定程式執行區塊的範圍、以及提供橫切面的功能(cross-cutting concerns)。這些到處都需要的物件或服務,不太可能一一注入到每個需要它們的地方:一來過於繁瑣,二來有些子模組或程式區塊是碰觸不到、或不在控制範圍內的。因此,Ambient Context 沒有明顯「注入物件」的味道;它不是侵入性的,而是在某個地方已經準備好、被動地等著別人來取用。此特性在某些場合正好可以彌補前述注入方式的不足,故在此一併討論。
已知應用例
.NET 類別庫中提供交易管理功能的 System.Transactions.TransactionScope 就是 Ambient Context 的一個例子。以下程式片段示範了基礎用法。
using (TransactionScope trxScope = new TransactionScope())
{
// 執行多項資料異動作業。
order.Add(newOrder);
customer.LastOrderDate = DateTime.Now;
trxScope.Complete(); // 確認交易。
}
此外,ASP.NET 應用程式經常會用 Http.Web.HttpContext.Current 來取得目前的 HttpContext 物件。這也是一個常見的例子。
範例程式(一)
如前面提過的,Ambient Context 模式可用於程式特定執行範圍內共享物件狀態,此「特定範圍」可以是整個應用程式、特定執行緒、或其他自訂的執行範圍。如果是整個應用程式範圍內皆可存取的共享物件,實作起來相當容易,通常用一個公開的靜態類別和靜態屬性就能達成。例如以下程式片段:
public static AppShared
{
private static ILogger _logger = new MyLogger();
public static ILogger Logger
{
get { return _logger; }
set { _logger = value; }
}
}
每當應用程式需要寫入日誌訊息時,在任何地方皆可使用如下方式達成:
AppShared.Logger.Info("請謹慎使用靜態變數和全域變數。");
範例程式(二)
這裡再提供一個範例,示範如何實作一個依個別執行緒(per thread)共享物件資訊的 Ambient Context 類別。此類別會使用 .NET Framework 4.0 之後提供的 ThreadLocal<T> 來保存個別執行緒的狀態資訊。
令此 Ambient Context 類別名稱為 PerThreadContext,而且它要提供一個靜態的 Current 屬性,供外界取得當前的 context 物件。如此一來,用戶端程式可以透過以下方式取得當前執行緒 context 中的共享物件:
var obj = PerThreadContext.Current.SomeMember;
PerThreadContext 類別的程式碼如下:
public class PerThreadContext
{
// 用一個靜態的 ThreadLocal<T> 來管理各執行緒的 context 物件。
private static ThreadLocal<PerThreadContext> _threadedContext;
static PerThreadContext()
{
_threadedContext = new ThreadLocal<PerThreadContext>();
}
// 共享的狀態
public DateTime OnceUponATime { get; set; }
// 把建構函式宣告為私有,不讓外界任意 context 物件。
private PerThreadContext()
{
OnceUponATime = DateTime.Now;
}
public static PerThreadContext Current
{
get
{
// 如果目前的執行緒中沒有 context 物件...
if (_threadedContext.IsValueCreated == false)
{
// 就建立一個,並保存至 thread-local storage。
_threadedContext.Value = new PerThreadContext();
}
return _threadedContext.Value;
}
}
}
這裡使用了延遲初始化(lazy initialization)的技巧:當用戶端程式透過靜態屬性 Current 取得當下的 context 物件時,先檢查目前的執行緒中有沒有 context 物件,有則直接傳回物件參考,若沒有,便建立一個,並保存至目前執行緒專屬的儲存區(thread-local storage)。其中的公開物件屬性 OnceUponATime 代表要與其他物件共享的狀態。
我們可以用一個簡單的 Console 程式來觀察其運作機制:
static void Main(string[] args)
{
ShowTime();
System.Threading.Thread.Sleep(2000);
var t1 = new Thread(ShowTime);
var t2 = new Thread(ShowTime);
t1.Start();
System.Threading.Thread.Sleep(2000);
t2.Start();
System.Threading.Thread.Sleep(2000);
ShowTime();
/* 執行結果:
Thread 1: 2014/5/4 下午 01:37:09
Thread 3: 2014/5/4 下午 01:37:11
Thread 4: 2014/5/4 下午 01:37:13
Thread 1: 2014/5/4 下午 01:37:09
*/
}
static void ShowTime()
{
Console.WriteLine("Thread {0}: {1} ",
Thread.CurrentThread.ManagedThreadId,
PerThreadContext.Current.OnceUponATime);
}
執行結果顯示,同樣是印出 PerThreadContext.Current.OnceUponATime 屬性值,不同的執行緒會有不同的結果。
Service Locator 模式
Service Locator(服務定位器)是一種設計模式,它同時具有前面提過的 Ambient Context 和 Factory 模式的性質,而且經常與 DI 搭配使用(儘管頗具爭議),故在此一併介紹。
顧名思義,Service Locator 的功能是用來尋找應用程式所需的服務,並返回該服務的執行個體。說得更具體些,當用戶端需要特定介面(或抽象類別)的物件時,既不使用 new 來建立物件,也不使用注入物件的機制,而是向 Service Locator 要一個物件。其基本運作機制如下:
用戶端向 Service Locator 提出請求,要求一個符合 IServiceA 的物件。
Service Locator 透過本身的型別搜尋/對應機制來尋找符合(相容於) IServiceA 介面的具象類別,然後建立該類別的物件實體,並回傳給用戶端。
下圖為 Service Locator 模式的結構圖。
Service Locator 經常以 Singleton 的方式實作。這裡我採用 static 類別的方式來實作一個極陽春的 Service Locator。你也可以將它視為全域共享的 Ambient Context。類別名稱就叫做 ServiceLocator,程式碼如下。
public static class ServiceLocator
{
public static object GetService(Type requestedType)
{
if (requestedType is IMessageService)
{
return new EmailService();
}
else
{
// 略
}
}
}
若把先前的 AuthenticationService 範例改成使用此 ServiceLocator 來取得符合 IMessageService 介面的服務,程式碼會像這樣:
class AuthenticationService
class AuthenticationService
{
private readonly IMessageService msgService;
// 原本使用建構式注入
public AuthenticationService(IMessageService service)
{
this.msgService = service;
}
// 現在改用 Service Locator
public AuthenticationService()
{
this.msgService = ServiceLocator.GetService(IMessageService);
}
}
由於這種寫法太方便了,我們甚至可能懶得在建構函式中取得物件參考並保存至私有變數,而變成在程式中的任何地方、任何時候呼叫 ServiceLocator 來取得物件。然而,這裡有兩個問題必須注意。
首先,程式的語意變得比較隱晦,因而增加理解上的困難。進一步說,用戶端由於不再需要傳入相依物件至類別的建構函式,所以「瞄一眼建構函式就知道類別依賴哪些型別」的優點已經消失。同樣地,從用戶端程式碼也通常不容易看出此類別需要哪些相依物件。換言之,Service Locator 模式把物件實體化的相關資訊都隱藏起來了;這也是前面提過的,Service Locator 具有 Factory 性質的原因。
第二個問題是,AuthenticationService 原本使用「建構式注入」時並未依賴任何實作類別,改用 Service Locator 模式之後,卻依賴特定的具象類別 ServiceLocator 了。若推而廣之,在應用程式中大量使用此模式來取得相依物件,就會變成到處都依賴這個 ServiceLocator 類別。這於種大量依賴同一個類別的情形,如果該類別不是自己寫的,而是採用第三方元件,就得更慎重考慮其穩定性,以及是否會增加日後維護的麻煩。
基於上述兩個原因,許多人建議 Service Locator少用為妙。Mark Seemann 甚至直接把這種用法歸類為「反模式」(anti-pattern)。
小結
接著要談的是〈過度注入的陷阱與迷思〉,但我想,本系列就連載到這一集為止吧。如果您有興趣閱讀其餘內容,可前往書籍簡介與購買資訊線上購買這本電子書。
Happy coding!
續上集,接著要談 Ambient Context 與 Service Locator 模式。
Ambient Context 模式
前述三種注入相依物件的方式,有些場合可能不適用,例如:應用程式特定執行環境的範圍內需要共享特定物件。碰到這種場合,便可以考慮採用 **Ambient Context **(環境脈絡)模式來解決。
Ambient Context又叫做 Context Object(環境物件),是一種常見的設計模式,主要用於跨階層、跨模組共享物件、界定程式執行區塊的範圍、以及提供橫切面的功能(cross-cutting concerns)。這些到處都需要的物件或服務,不太可能一一注入到每個需要它們的地方:一來過於繁瑣,二來有些子模組或程式區塊是碰觸不到、或不在控制範圍內的。因此,Ambient Context 沒有明顯「注入物件」的味道;它不是侵入性的,而是在某個地方已經準備好、被動地等著別人來取用。此特性在某些場合正好可以彌補前述注入方式的不足,故在此一併討論。
已知應用例
.NET 類別庫中提供交易管理功能的 System.Transactions.TransactionScope 就是 Ambient Context 的一個例子。以下程式片段示範了基礎用法。
using (TransactionScope trxScope = new TransactionScope())
{
// 執行多項資料異動作業。
order.Add(newOrder);
customer.LastOrderDate = DateTime.Now;
trxScope.Complete(); // 確認交易。
}
此外,ASP.NET 應用程式經常會用 Http.Web.HttpContext.Current 來取得目前的 HttpContext 物件。這也是一個常見的例子。
範例程式(一)
如前面提過的,Ambient Context 模式可用於程式特定執行範圍內共享物件狀態,此「特定範圍」可以是整個應用程式、特定執行緒、或其他自訂的執行範圍。如果是整個應用程式範圍內皆可存取的共享物件,實作起來相當容易,通常用一個公開的靜態類別和靜態屬性就能達成。例如以下程式片段:
public static AppShared
{
private static ILogger _logger = new MyLogger();
public static ILogger Logger
{
get { return _logger; }
set { _logger = value; }
}
}
每當應用程式需要寫入日誌訊息時,在任何地方皆可使用如下方式達成:
AppShared.Logger.Info("請謹慎使用靜態變數和全域變數。");
範例程式(二)
這裡再提供一個範例,示範如何實作一個依個別執行緒(per thread)共享物件資訊的 Ambient Context 類別。此類別會使用 .NET Framework 4.0 之後提供的 ThreadLocal<T> 來保存個別執行緒的狀態資訊。
令此 Ambient Context 類別名稱為 PerThreadContext,而且它要提供一個靜態的 Current 屬性,供外界取得當前的 context 物件。如此一來,用戶端程式可以透過以下方式取得當前執行緒 context 中的共享物件:
var obj = PerThreadContext.Current.SomeMember;
PerThreadContext 類別的程式碼如下:
public class PerThreadContext
{
// 用一個靜態的 ThreadLocal<T> 來管理各執行緒的 context 物件。
private static ThreadLocal<PerThreadContext> _threadedContext;
static PerThreadContext()
{
_threadedContext = new ThreadLocal<PerThreadContext>();
}
// 共享的狀態
public DateTime OnceUponATime { get; set; }
// 把建構函式宣告為私有,不讓外界任意 context 物件。
private PerThreadContext()
{
OnceUponATime = DateTime.Now;
}
public static PerThreadContext Current
{
get
{
// 如果目前的執行緒中沒有 context 物件...
if (_threadedContext.IsValueCreated == false)
{
// 就建立一個,並保存至 thread-local storage。
_threadedContext.Value = new PerThreadContext();
}
return _threadedContext.Value;
}
}
}
這裡使用了延遲初始化(lazy initialization)的技巧:當用戶端程式透過靜態屬性 Current 取得當下的 context 物件時,先檢查目前的執行緒中有沒有 context 物件,有則直接傳回物件參考,若沒有,便建立一個,並保存至目前執行緒專屬的儲存區(thread-local storage)。其中的公開物件屬性 OnceUponATime 代表要與其他物件共享的狀態。
我們可以用一個簡單的 Console 程式來觀察其運作機制:
static void Main(string[] args)
{
ShowTime();
System.Threading.Thread.Sleep(2000);
var t1 = new Thread(ShowTime);
var t2 = new Thread(ShowTime);
t1.Start();
System.Threading.Thread.Sleep(2000);
t2.Start();
System.Threading.Thread.Sleep(2000);
ShowTime();
/* 執行結果:
Thread 1: 2014/5/4 下午 01:37:09
Thread 3: 2014/5/4 下午 01:37:11
Thread 4: 2014/5/4 下午 01:37:13
Thread 1: 2014/5/4 下午 01:37:09
*/
}
static void ShowTime()
{
Console.WriteLine("Thread {0}: {1} ",
Thread.CurrentThread.ManagedThreadId,
PerThreadContext.Current.OnceUponATime);
}
執行結果顯示,同樣是印出 PerThreadContext.Current.OnceUponATime 屬性值,不同的執行緒會有不同的結果。
Service Locator 模式
Service Locator(服務定位器)是一種設計模式,它同時具有前面提過的 Ambient Context 和 Factory 模式的性質,而且經常與 DI 搭配使用(儘管頗具爭議),故在此一併介紹。
顧名思義,Service Locator 的功能是用來尋找應用程式所需的服務,並返回該服務的執行個體。說得更具體些,當用戶端需要特定介面(或抽象類別)的物件時,既不使用 new 來建立物件,也不使用注入物件的機制,而是向 Service Locator 要一個物件。其基本運作機制如下:
用戶端向 Service Locator 提出請求,要求一個符合 IServiceA 的物件。
Service Locator 透過本身的型別搜尋/對應機制來尋找符合(相容於) IServiceA 介面的具象類別,然後建立該類別的物件實體,並回傳給用戶端。
下圖為 Service Locator 模式的結構圖。
Service Locator 經常以 Singleton 的方式實作。這裡我採用 static 類別的方式來實作一個極陽春的 Service Locator。你也可以將它視為全域共享的 Ambient Context。類別名稱就叫做 ServiceLocator,程式碼如下。
public static class ServiceLocator
{
public static object GetService(Type requestedType)
{
if (requestedType is IMessageService)
{
return new EmailService();
}
else
{
// 略
}
}
}
若把先前的 AuthenticationService 範例改成使用此 ServiceLocator 來取得符合 IMessageService 介面的服務,程式碼會像這樣:
class AuthenticationService
class AuthenticationService
{
private readonly IMessageService msgService;
// 原本使用建構式注入
// 現在改用 Service Locator
public AuthenticationService()
{
this.msgService = ServiceLocator.GetService(IMessageService);
}
}
Note: 第 3 章介紹自製 DI 容器時,會有比較像樣的實作範例。
由於這種寫法太方便了,我們甚至可能懶得在建構函式中取得物件參考並保存至私有變數,而變成在程式中的任何地方、任何時候呼叫 ServiceLocator 來取得物件。然而,這裡有兩個問題必須注意。
首先,程式的語意變得比較隱晦,因而增加理解上的困難。進一步說,用戶端由於不再需要傳入相依物件至類別的建構函式,所以「瞄一眼建構函式就知道類別依賴哪些型別」的優點已經消失。同樣地,從用戶端程式碼也通常不容易看出此類別需要哪些相依物件。換言之,Service Locator 模式把物件實體化的相關資訊都隱藏起來了;這也是前面提過的,Service Locator 具有 Factory 性質的原因。
第二個問題是,AuthenticationService 原本使用「建構式注入」時並未依賴任何實作類別,改用 Service Locator 模式之後,卻依賴特定的具象類別 ServiceLocator 了。若推而廣之,在應用程式中大量使用此模式來取得相依物件,就會變成到處都依賴這個 ServiceLocator 類別。這於種大量依賴同一個類別的情形,如果該類別不是自己寫的,而是採用第三方元件,就得更慎重考慮其穩定性,以及是否會增加日後維護的麻煩。
基於上述兩個原因,許多人建議 Service Locator少用為妙。Mark Seemann 甚至直接把這種用法歸類為「反模式」(anti-pattern)。
小結
接著要談的是〈過度注入的陷阱與迷思〉,但我想,本系列就連載到這一集為止吧。如果您有興趣閱讀其餘內容,可前往書籍簡介與購買資訊線上購買這本電子書。
Happy coding!
這系列好棒,讀來很受用,謝謝版主
回覆刪除很高興有幫助。謝謝 :)
回覆刪除