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


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

上集,接著要談 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);
    }
}  
Note: 第 3 章介紹自製 DI 容器時,會有比較像樣的實作範例。

由於這種寫法太方便了,我們甚至可能懶得在建構函式中取得物件參考並保存至私有變數,而變成在程式中的任何地方、任何時候呼叫 ServiceLocator 來取得物件。然而,這裡有兩個問題必須注意。

首先,程式的語意變得比較隱晦,因而增加理解上的困難。進一步說,用戶端由於不再需要傳入相依物件至類別的建構函式,所以「瞄一眼建構函式就知道類別依賴哪些型別」的優點已經消失。同樣地,從用戶端程式碼也通常不容易看出此類別需要哪些相依物件。換言之,Service Locator 模式把物件實體化的相關資訊都隱藏起來了;這也是前面提過的,Service Locator 具有 Factory 性質的原因。

第二個問題是,AuthenticationService 原本使用「建構式注入」時並未依賴任何實作類別,改用 Service Locator 模式之後,卻依賴特定的具象類別 ServiceLocator 了。若推而廣之,在應用程式中大量使用此模式來取得相依物件,就會變成到處都依賴這個 ServiceLocator 類別。這於種大量依賴同一個類別的情形,如果該類別不是自己寫的,而是採用第三方元件,就得更慎重考慮其穩定性,以及是否會增加日後維護的麻煩。

基於上述兩個原因,許多人建議 Service Locator少用為妙。Mark Seemann 甚至直接把這種用法歸類為「反模式」(anti-pattern)。

小結

接著要談的是〈過度注入的陷阱與迷思〉,但我想,本系列就連載到這一集為止吧。如果您有興趣閱讀其餘內容,可前往書籍簡介與購買資訊線上購買這本電子書。

Happy coding!