GooglePrettify

顯示具有 IoC 標籤的文章。 顯示所有文章
顯示具有 IoC 標籤的文章。 顯示所有文章

2017年3月2日 星期四

Dependency Injection 筆記 (6)

.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!

from : http://huan-lin.blogspot.com/2011/11/dependency-injection-6.html

Dependency Injection 筆記 (5)

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


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

上集,介紹完幾個相關設計模式之後,接著要來看 DI 的模式,亦即注入物件的方式。

注入方式

DI 的核心概念是寬鬆耦合,是「針對介面寫程式」,故一旦開始在程式中運用 DI 技術,你可能會開始對「new 一個物件」的寫法更敏感。你可能會開始考慮,這個地方如果用 new 來建立特定實作類別的物件,將來需要修改程式時會不會很麻煩?如果只依賴介面或抽象類別會不會比較好?一旦開始出現這種現象,您已經朝向寬鬆耦合之路邁進了。

本節將介紹 DI 的三種注入方式,包括:
  • 建構式注入(Constructor Injection)
  • 屬性注入(Property Injection)
  • 方法注入(Method Injection)

這三種注入方式基本上都符合下圖所描繪的模式。


建構式注入

建構式注入(Constructor Injection)指的是類別所需要的物件是由外界透過該類別的公開建構函式傳入。若需要注入 N 個相依物件,則建構函式通常至少要有 N 個引數,且引數的型別為介面或抽象類別。

已知應用例

.NET 基礎類別庫的 System.IO.Compression.ZipArchive 類別有一個建構函式提供了外界注入物件的機制,其函式原型如下:

public ZipArchive(Stream stream)

用法

假設類別 AuthenticationService 需要使用符合 IMessageService 介面的物件,可按以下步驟實現「建構式注入」:
  1. 在 AuthenticationService 類別中宣告一個型別為 IMessageService 的成員變數。令此成員變數名稱為 msgService。
  2. 撰寫建構函式,在參數列中加入一個 IMessageService 型別的參數,然後將此參數值設定給成員變數 msgService。

範例程式

class AuthenticationService
{
    private readonly IMessageService msgService;

    public AuthenticationService(IMessageService service)
    {
        if (service == null)
        {
            throw new ArgumentException("service");
        }
        this.msgService = service;
    }
}

程式說明:

  • 在此範例中,AuthenticationService 提供了建構式注入的方式,讓外界得以傳入一個符合 IMessageService 介面的物件。若外界傳入不相容於此介面的型別,程式碼將無法通過編譯。
  • 私有成員 msgService 的變數宣告之所以加上 readonly 關鍵字,是為了確保相依物件一旦在 AuthenticationService 的建構函式中設定完成後,就不能再改變。
  • 建構函式還檢查了傳入的相依物件是否為 null,以確保一旦建構函式執行完畢,在此類別中的任何地方都可以使用相依物件,而無須擔心它是否為無效參考。
Note: 每當需要注入相依物件時,一般建議優先考慮「建構式注入」,因為其用法對呼叫端來說相當明確、直覺——建立物件時就要一併傳入所有相依物件,所以呼叫端透過建構函式便可得知某物件相依於哪些第三方元件。
屬性注入

顧名思義,屬性注入(Property Injection)就是透過物件的屬性來注入相依物件,另一個稱呼是「設定函式注入」(Setter Injection)。它與「建構式注入」相似,類別本身也需要有個成員變數來保存對相依物件的參考,但有個主要區別:「屬性注入」的時機比「建構式注入」來得晚,而且外界不一定會設定該屬性。也就是說,如欲採用「屬性注入」,類別本身通常要能夠取得相依物件的預設實作。如此一來,即使外界沒有注入相依物件,類別仍有預設的相依物件可用。

已知應用例

ASP.NET MVC 的 ControllerBuilder.SetControllerFactory 方法。此方法可用來切換 ASP.NET MVC 的 DefaultControllerFactory`(第 4 章有實作範例)。此方法之原型宣告如下:

public void SetControllerFactory(IControllerFactory controllerFactory)

用法

在類別中定義一個公開屬性,以便用戶端可以隨時設定該屬性來切換相依物件(或完全不設定)。若用戶端未曾設定該屬性,則由類別本身提供預設實作,或撰寫 null 檢查邏輯來避免執行時期發生 NullReferenceException。

範例程式

class AuthenticationService
{
    private IMessageService msgService;

    public IMessageService MessageService
    {
        get { return this.msgService; }
        set { this.msgService = value; }
    }

    public void Login(string userId, string password)
    {
        MessageService.Send(...);  // 使用相依物件。
    }
}

屬性 MessageService 背後所使用的私有成員 msgService 不能宣告為 readonly,因為「屬性注入」允許外界於任何時候注入相依物件,甚至切換相依物件或根本不注入相依物件。對於不注入相依物件的情況,類別本身必須做些防護措施,以免其他地方存取相依物件時,因物件參考為 null 而引發 NullReferenceException 或其他異常。常見的防護措施是由類別本身提供預設的相依物件,時機可以選在建構函式中設定,或者更晚一點,在屬性的 getter 區塊中設定,參考以下程式範例:

public IMessageService MessageService
{
    get
    {
        if (this.msgService == null)
        {
            this.msgService = new DefaultMessageService();
        }
        return this.msgService;
    }
    set { this.msgService = value; }
}

只是,如此一來,此類別又和特定實作 DefaultMessageService 綁在一起了。如果你覺得這種寫法不好,亦可考慮使用稍後介紹的 Method Injection(方法注入)或 Ambient Context(環境脈絡),或先前提過的 Factory 模式來解決,例如 Factory Method。


看完「建構式注入」和「屬性注入」這兩個小節之後,如果您熟悉設計模式,會不會有一種感覺:DI 骨子裡根本就是 Strategy 模式嘛!底下附上 Strategy 模式的結構圖,讀者不妨把它當作一個練習,揣摩看看兩者的異同。


方法注入

「方法注入」(Method Injection)指的是用戶端每次呼叫某物件的方法時都必須透過方法的引數來傳入相依物件。此注入方式的適用時機如下:

  • 提供服務的類別不需要在整個類別中使用特定相依物件,而只有在用戶端呼叫某些方法時才需要傳入那些物件。
  • 用戶端每次呼叫特定方法時可能會傳入不同的物件,而這些物件唯一相同之處是它們都實作了同一個介面(或繼承自同一個抽象類別)。

已知應用例

ADO.NET 的 DbDataAdapter.Fill 方法有提供「方法注入」的機制,其函式原型如下:

protected virtual int Fill(
    DataTable dataTable,     // 查詢結果會填入此物件。
    IDbCommand command,      // 提供查詢命令的物件。
    CommandBehavior behavior // 細部控制命令的行為。
)

用法

針對類別中的特定方法,把需要的相依物件加入方法的參數列。用戶端在每次呼叫這些方法時必須建立並傳入這些相依物件。

採用此注入方式時,除了傳入相依物件之外,經常會一併傳入其他相關的參數值,以便完成特定任務。例如:

public void ExecuteTask(ITaskContext context, int value1, int value2)
{
    // 略
}

範例

仍以先前的 AuthenticateService 為例,如果 Login 方法允許外界指定使用何種驗證碼發送機制,便可改成這樣:

public void Login(string userId, string password, IMessageService service)
{
    if (service == null)
    {
        throw new ArgumentException("service");
    }       
    service.Send(...);
}


軟體框架和底層基礎建設(infrastructure)類型的函式庫也經常用到「方法注入」,因為這些類別庫通常不需要保存外界所提供的相依物件,而大多是針對每一次方法呼叫來讓多個物件共同達成任務。這同時也意味著,那些在方法呼叫之間傳遞的相依物件,通常也是在某高階模組需要完成特定工作時才臨時建立,而不會在應用程式的進入點或初始化階段就全都準備好這些物件。另一方面,如前兩節討論過的,提供「建構式注入」和「屬性注入」的類別則通常會在類別本身利用一個私有成員變數來保存外界注入的相依物件,而那些相依物件很有可能是高階模組初始化的時候、甚至在應用程式一開始執行時就已經預先建立的。

接著要介紹的是 Ambient Context 與 Service Locator 模式。

from : http://huan-lin.blogspot.com/2011/11/dependency-injection-5.html

Dependency Injection 筆記 (4)

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


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

上集未完的相關設計模式...

Composite 模式


延續先前的電器比喻。現在,如果希望 UPS 不只接電腦,還要接電風扇、除濕機,可是 UPS 卻只有兩個電源輸出孔,怎麼辦?

我們可以買一條電源延長線,接在 UPS 上面。如此一來,電風扇、除濕機、和電腦便都可以同時插上延長線的插座了。這裡的電源延長線,即類似Composite Pattern(組合模式),因為電源延長線本身又可以再連接其他不同廠牌的延長線(這又是因為插座皆採用相同介面),如此不斷連接下去。

呃….延長線的比喻有個小問題:它在外觀上看起來也像是層層串接,容易和 Decorator 模式混淆。事實上,這兩種設計模式在結構上的確有相似之處。下圖所示為 Composite 模式的結構圖。


由此結構圖可以看得出來,Composite 模式 其實是個樹狀結構,呈現的是「整體-包含」(whole-part)的關係。樹上的每個節點(Leaf)都實作了相同的介面,而每個節點又可以包含多個子節點;就像檔案目錄結構那樣,每個資料夾底下都可以有零至多個資料夾。相較之下,Decorator 模式則是讓裝飾者看起來長得和被裝飾者一樣,但其實加上了額外的修飾。

Adapter 模式

當你的手機沒電,需要充電時,就算有電源延長線也沒用,因為手機充電時所需的電壓並不是一般家庭用電的 110 伏特交流電壓。此時我們通常會使用手機隨附的變壓器(adapter),將變壓器的電源插頭插在牆壁的電源插座,然後將變壓器的另一端連接至手機。像這樣把一種規格(介面)轉換成另一種規格的設計,就叫做Adapter Pattern(轉換器模式)。下圖所示為 Adapter 模式的結構。

仍使用先前 logging 範例來說明。假設我們沒有實作自己的 logging API,而是直接使用第三方元件。然而,考慮到將來很可能會改用另一套 logging 元件,於是決定使用 Adapter 模式來保護自己的程式碼。首先,必須先訂出 logging API 的介面,讓應用程式只針對此介面來寫入 log。此介面只定義了一個寫入日誌的方法,叫做 Log,參考以下程式片段。

public interface ILogger
{
    void Log(string msg);
}

接著設計 Adapter 類別。此類別須實作 ILogger 介面,並且在 Log 方法中轉而呼叫第三方元件的方法。程式碼如下:

public class CommonLogger : ILogger
{
    private ThirdPartyLogger logger = new ThirdPartyLogger();

    public void Log(string msg)
    {
        logger.WriteEntry(msg);  // 轉呼叫第三方元件的方法。
    }
}

如此一來,以後如果真的需要改用另一套 logging 元件,程式修改的範圍就只限定在 CommonLogger 類別而已。

Factory 模式

第一章曾經提過,每當我們在程式中使用 new 運算子來建立類別的執行個體,我們的程式碼就在編譯時期跟那個類別固定綁(繫結)在一起了。其實用 new 來建立物件還有個缺點:C# 的建構函式名稱就是類別名稱,不可任意命名;於是當類別有數個多載的(overloaded)建構函式時,光是閱讀傳入建構函式的參數列,有時不見得那麼容易明白程式的意圖。舉例來說:

var user1 = new User("Mike", 101, true);
var user2 = new User("Jane", 102, flase);

不如下列程式碼清楚:

var user1 = UserFactory.CreateAdministrator("Mike", 101);
var user2 = UserFactory.CreateDomainUser("Jane", 102);

其中的 UserFactory 就是擔任物件工廠的角色,它是個 static 類別,且唯一的任務就是生產特定類型的物件。程式碼如下所示。

public static class UserFactory
{
    public User CreateAdministrator(string name, int id)
    {
        // 略
    }

    public User CreateDomainUser(string name, int id)
    {
        // 略
    }
}

一般而言,Factory 模式泛指各種能夠生產物件的工廠,通常有三種模式:Factory Method(工廠方法)、Simple Factory(簡單工廠)、和 Abstract Factory(抽象工廠)。剛才的 UserFactory 就是一個 Simple Factory。
Note: 假設我們完全不知道 DI,或者覺得沒必要使用 DI,可是又希望程式碼不要和特定實作類別綁太緊(不想要直接 new 一個物件),此時 Factory 模式通常是個值得考慮的方案。
實作 Factory Method 模式時,通常會在一個基礎類別中定義建立物件的抽象方法(難怪叫做「工廠方法」),然後由各個子類別來實作該方法。若將先前的 UserFactory 改成以 Factory Method 來實作,其類別結構如下圖所示。

Abstract Factory 比前面兩種工廠模式要稍微複雜一些,它是用來建立多族系的相關或相依物件,且無須指名物件的具象類別。實作此模式時,會將一組建立物件的方法定義成一個介面,代表抽象工廠。然後,你可以撰寫多個類別來實作該介面,而這些類別的角色就像真實世界中的工廠,類別中的每一個工廠方法則有點像是真實工廠裡的一條生產線。當用戶端需要建立該族系的物件時,就是利用其中一種具象工廠(concrete factory)來生成物件。此外,由於具象工廠都實作了同一組介面,所以用戶端甚至可以在執行時期動態切換成不同的工廠,以建立一組相關的物件。
如果你跟我一樣,常常搞混這三種 Factory 模式,我發現《Refactoring to Patterns》這本書的 6.2 節裡面有一張簡略的結構圖挺有用。我依樣畫了一張,如下圖所示,其中的粗黑線代表建立物件的函式。下次忘記時,不妨回來瞄一眼底下這張圖,也許能幫你回想起來它們之間的差異。


OK! 設計模式的部分就概略介紹到此,後續章節中如碰到其他模式,也會一併介紹(例如 Strategy、Repository、Service Locator 等等)。

「我曾在這樣的十字路口:努力學習各種模式,希望成為一個更好的軟體設計師;但現在,為了真正成為更優秀的軟體設計師,我必須降低對模式的依賴。」
—— Joshua Kerievsky. 《Refactoring to Patterns》 作者


from : http://huan-lin.blogspot.com/2011/10/dependency-injection-4.html

Dependency Injection 筆記 (3)

.NET 相依性注入》的試閱章節連載 (3)


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

上集,本文開始進入第二章。

讀完第 1 章以後,您應該已經了解 DI 的用途與目的,接著要來進一步了解的是 DI 的實作技術,也就是注入相依物件的方式。本章所介紹的相依性注入方式,又稱為「窮人的 DI」(poor man’s DI),因為這些用法都與特定 DI 工具無關,亦即不使用任何現成的 DI 框架(例如 Unity、Autofac)。畢竟,DI 只是一組設計原則與模式,不依賴任何工具也能實現。
本章範例原始碼位置:https://github.com/huanlin/di-book-support 裡面的 Examples/ch02 資料夾。

設計模式梗概
每個模式都描述了一個不斷發生在我們周遭的問題,然後描述該問題的核心解法,於是你便可以一再使用該解法,而無須對同樣的事情做兩次工。
—— Christopher Alexander. A Pattern Language.

除了第 1 章提到的 S.O.L.I.D. 設計原則,在運用 DI 技術時,也經常需要搭配一些設計模式(design patterns),例如 Factory Method(工廠方法)、Decorator(裝飾)、Composite(組合)、Adapter(轉換器)等等。基於後續章節討論的必要,本節將介紹幾個相關的設計模式。如需比較完整深入的介紹,可參考相關書籍,例如:《物件導向設計模式》、《深入淺出設計模式》、《重構-向範式前進》等等。

小引-電器與介面

日常生活中,四處可見電器用品,例如電視、微波爐、電腦等等。這些電器通常都有條電線,電線尾端是個插頭,而當我們要使用這些電器時,就把插頭插在牆壁或電源插座上,電器便能夠獲得所需之電力。一般情況下,沒有人會捨插座不用,而把電器的電源線固定焊在牆壁的電源插座。假使真這麼做,萬一有一天電視或電腦故障而需要維修,那可就麻煩了。

不只電源插座,電腦的 USB 插槽也一樣——它們都具備寬鬆耦合的特性。這裡的電源插座或 USB 插槽,對應到軟體世界裡的概念,便是介面。一個介面就等於是一份規格,而各家廠商所生產的各式各樣的電源插座或 USB 插槽,就是遵照其標準規格(介面)所實作出來的產品,或簡稱實作品。用軟體的術語來說,這些實作品就是類別——實作了特定介面的類別。

介面的威力即在於一旦訂出標準規格,各家廠商便可依照標準介面來製作各類產品。對使用者來說,好處則是享有多種選擇,因為他們不會被特定廠商的產品綁住;只要他們高興,隨時可以更換不同的產品,而且通常是隨插即用。在軟體的世界裡,介面也有同樣的好處:讓類別與類別之間保持寬鬆耦合,以便提供隨時抽換實作類別的彈性。

Null Object 模式

回到電源插座的例子。如果我們將電腦的電源線從插座上拔起,它們就只是彼此不再連接而已,電腦和插座並不會因此而著火或爆炸。但是在軟體程式的世界裡,若物件 A 會呼叫物件 B(物件 A 依賴物件 B),而當你將物件 B 移除,亦即物件 B 不存在時,程式就會發生 NullReferenceException 類型的錯誤。於是,我們常常會在程式裡面加入檢查物件參考是否為 null 的邏輯,例如:

?
1
2
3
4
if (anObject != null)
    anObject.DoSomething();
else
    DoSomethingElse();

如果在程式中一再重複寫這些檢查 null 的邏輯,程式碼便會膨脹,而且在解讀程式的主要邏輯時,常常得要跳過這些檢查邏輯,多少會形成閱讀程式碼的阻礙。針對此問題,我們可以設計一個空的、完全不做任何事的類別,然後在變數有可能是 null 的地方,讓它們指向那個空的物件。這種模式叫做 Null Object。
Null Object 的優點:可減少撰寫判斷物件參考是否為 null 的防錯邏輯。但前提是開發人員得知道有 Null Object 可用,否則還是會寫出多餘的防錯程式碼。
Null Object 類別通常要實作某個介面(或繼承自抽象類別),但實作程式碼完全沒做任何事,即所有方法都只是個空殼子,或僅提供無害的預設行為。以程式中常用的 logging(日誌)機制為例,我們可以將寫入日誌的操作定義成一個 ILogger 介面,然後依實際需要實作不同的 logging 類別,例如用來將日誌訊息輸出至 Console 的 ConsoleLogger。此外,考慮到應用程式有時候可能不需要紀錄任何訊息,我們可以實作一個 NullLogger 類別,當作 Null Object 使用。結構圖如下。


底下分別是 ILoger 介面以及 NullLogger 和 ConsoleLogger 類別的程式碼:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface ILogger
{
    Log(string msg);
}
 
public class NullLogger : ILogger
{
    public void Log(string msg)
    {
        // 不做任何事
    }
}
 
public class ConsoleLogger : ILogger
{
    public void Log(string msg)
    {
        Console.WriteLine(msg);
    }
}   

像底下這個函式,呼叫端只要傳入 ConsoleLogger 物件,日誌訊息就會輸出至 Console;而當呼叫端想要停止記錄日誌,便可傳入 NullLogger 物件。如此一來,就不用在每次寫入日誌訊息時都重複寫一遍檢查 logger 物件是否為 null 的防錯邏輯。

?
1
2
3
4
5
void DoSomething(ILogger logger)
{
    logger.Log("開始執行 DoSomething 函式。");
    ....
}
 
Note: Null Object 本身並不需要「進化」成真正有做事的物件,因為它的存在就是為了提供一個完全不做任何事、不具任何意義的物件。

Decorator 模式 

一般情況下,如果在使用電腦時突然停電了,尚未儲存的資料就會消失不見。為了解決此問題,我們可以在牆壁的電源插座與電腦電源線之間加入一個不斷電系統(UPS)。此時,UPS 的電源線會接在牆壁的電源插座上,而電腦的電源則改接在 UPS 上。此三者在串接的時候,都是透過單一的標準介面:插座。類似這種透過同一介面來串接多個不同物件的作法,叫做Decorator Pattern(裝飾模式)。此模式可以讓我們為物件層層疊加新的功能上去,而無須修改既有的類別。下圖為 Decorator 模式 的結構圖。 

延續前面的 logging 範例,假設想要在每次輸出 log 訊息時額外加上當時的日期時間,而且前提是不可修改既有的 ILogger 和 ConsoleLogger 類別,該怎麼做?

我們可以使用 Decorator 模式。作法為:設計一個新的類別,此類別不僅要實作 ILogger 介面,而且還需要使用既有的 ConsoleLogger 物件來輸出 log 訊息。簡單起見,我就把這個類別命名為 DecoratedLogger。程式碼如下: 
?
1
2
3
4
5
6
7
8
9
10
11
12
public class DecoratedLogger : ILogger
{
    private ILogger logger;
    public DecoratedLogger(ILogger aLogger)
    {
        logger = aLogger;
    }
    public void Log(string msg)
    {
        logger.Log(DateTime.Now.ToString() + " - " + msg);
    }
}

下圖描繪了這個簡略版本的 Decorator 模式範例的類別結構: 

於是,在用戶端程式中使用這個新的 DecoratedLogger 來輸出 log 訊息時,可以這麼寫:
?
1
2
3
4
5
void DoSomething()
{
    ILogger logger = new DecoratedLogger(new ConsoleLogger());
    logger.Log("Hello, 裝飾模式!");
}

你可以看到,在這次的修改當中,既有的 ILogger 和 ConsoleLogger 完全沒有動到。我們只增加了一個新類別(DecoratedLogger),就為應用程式加上了新功能。這也就符合了第 1 章提過的 OCP(開放/封閉原則)。 

from : http://huan-lin.blogspot.com/2011/10/dependency-injection-3.html