2011年10月28日星期五

Dependency Injection 筆記 (4)

上一次,我們已經分別看過非 DI 和 DI 版本的入門範例。若單純以程式碼的數量來比較,修改後的 DI 版本比原先的非 DI 版本多出了 20% 的程式碼。也許有人會懷疑:「這樣寫真的比較好嗎?不是都說寫程式應該 keep it simple and stupid 嗎?這樣寫豈不是更複雜了?」

聲明:這是我閱讀 Dependency Injection in .NET 一書的筆記,寫作的內容架構與靈感多來自本書。若覺得這些筆記對您有幫助,主要應歸功於該書的作者 Mark Seemann。筆記內容可能會持續修訂更新,請勿以複製全文的方式轉載。

殺雞用牛刀?沒那麼簡單

嗯,改成 DI 版本之後,程式碼是變多了,沒錯。儘管我已經在前文中提過,「寫 log」這個動作,未來有可能會是透過 e-mail 寄送,亦可能輸出至 Windows 事件日誌,但也許讀者心中不免仍有疑慮:為什麼殺雞要用牛刀?為什麼不採用比較簡單的程式碼呢?

首先,這個入門範例的需求本來就很單純,因此難免有人會覺得「其實這樣寫就可以 work 了,這樣就夠了。」然而,在軟體的世界裡,需求變動是家常便飯,我們總得稍微想一下,這樣的寫法,在目前的需求情境(context)之下,程式碼能夠撐多久不用改。
相關文章:YAGNI 原則的一點想法
其次,我曾碰過有人抱怨:「三層式的程式架構哪有比較好?UI 和作業流程、資料邏輯切那麼開,我連要找某個按鈕點下去所執行的 SQL 指令都找不到。幹嘛搞那麼複雜?」碰到這種情況,老實說,我覺得可能要費好大一番工夫解釋,恐怕不是光「成本效益比」和「軟體設計就是取捨」三兩句話就能說服對方。總之,就那句老生常談:「簡單這回事,從來都不是那麼簡單。」

喜歡直接看程式碼的朋友一定會嫌我囉嗦了。接著就用一些程式碼來說明 DI 的幾個好處,包括:晚期繫結、擴充性、可維護性、以及可測試性。

晚期繫結

上一篇文章的 Hello DI 範例其實沒有使用晚期繫結(late binding),但由於我們已經使用了 DI 的寫法,它離晚期繫結也只有一步之遙,只需再加一點程式碼就能達成。這裡我打算用比較簡單的做法來示範晚期繫結:將欲使用之 logger 類別的名稱寫在應用程式組態檔中,然後在執行時期取得型別名稱,並動態建立該型別的 instance。

首先,把我們的 ConsoleLogger 類別的全名寫在應用程式組態檔案裡,像這樣:

  <appSettings>
    <add key="LoggerTypeName" value="HelloDI.ConsoleLogger, HelloDI" />
  appSettings>

其中的 value 屬性值包含兩個部分,逗號前面的是類別全名,後面則是組件名稱。

接下來,在 Main 方法中,原本的這行程式碼:

    ILogger logger = new ConsoleLogger();

要改成這樣:

    var typeName = ConfigurationManager.AppSettings["LoggerTypeName"];
    Type aType = Type.GetType(typeName);
    ILogger logger = (ILoggerActivator.CreateInstance(aType);

如此一來,應用程式欲使用哪一種 logger,就只要在組態檔中指定就行了。任何時候要改用別的 logger,例如 WindowsEventLogger,也只要改組態檔,而無須修改程式碼。
Note: 這種動態建立物件的方法有個限制:類別非得提供預設建構式不可。此限制所衍生的問題,將來有機會再細談。
順便一提,這裡並沒有「晚期繫結絕對比早期繫結好」的意思。重點在於,使用 DI 能夠提供這種彈性:當專案時程緊迫,沒有太多工夫精雕細琢時,我們可以先使用比較陽春的 DI 寫法;等到需要晚期繫結時再加上去就行了。

擴充性

好的設計通常很容易修改和擴充。以先前的 HelloDI 為例,假設我們現在想要在每次輸出 log 訊息時,額外加上當時的日期時間,而且前提是不要修改既有的 ILogger 和 ConsoleLogger 類別,這該怎麼做?

我們可以使用 Decorator 模式。作法為:設計一個新的類別,此類別不僅要實作 ILogger 介面,而且還需要使用既有的 ConsoleLogger 物件來輸出 log 訊息。簡單起見,我就把這個類別命名為 DecoratedLogger。程式碼如下:

    public class DecoratedLogger : ILogger
    {
        private ILogger logger;
 
        public DecoratedLogger(ILogger aLogger)
        {
            logger = aLogger;
        }
 
        public void WriteEntry(string msg)
        {
            logger.WriteEntry(DateTime.Now.ToString() + " - " + msg);
        }
    }

沒有更動的部分(例如 ILogger  和 ConsoleLogger),這裡就不再重複列出程式碼了。若有需要,請回頭參閱先前的文章。

你可以從這個 DecoratedLogger 類別的程式碼大略看出 Decorator 模式的用法:類別本身實作了某個介面(或繼承某基礎類別),同時又握有該介面(或基礎類別)的物件參考。或者換個方式說:在 Decorator 模式中,裝飾者類別不僅是一種(is-a)元件,它同時也包含了(has-a)那種元件的執行個體。底下附上 Decorator 模式的類別圖,方便跟 DecoratedLogger 類別的程式碼相互對照。


於是,在 Main 函式中使用這個新的  DecoratedLogger  類別來輸出 log 訊息時,可以這麼寫:

        private static void Main(string[] args)
        {
            ILogger log = new DecoratedLogger(new ConsoleLogger());
            log.WriteEntry("Hello, DI!");
        }

程式的輸出結果如下:

2011/10/28 上午 12:40:42 - Hello, DI!

你可以看到,既有的 ILogger 和 ConsoleLogger 完全沒有動到。既有程式碼唯一要修改的地方,就是主程式的 Main 方法;而由於這裡是組合各元件的地方,為了方便溝通,就有人給它取了名稱,叫做:Composition Root(此系列若能撐到 DI Container,會再進一步說明此概念)。

在這次的修改當中,我們只增加一個新類別(DecoratedLogger),就為既有的程式加上了新功能,原有的類別則完全沒有更動。這種寬鬆耦合的設計,能夠很容易就讓我們寫出符合開放封閉原則(Open-Closed Principle;簡稱 OCP)的程式。所謂的開放封閉原則,指的就是模組(或類別)應敞開擴充大門,但關閉修改之窗。

可維護性

可維護性的部分,就從單一責任原則(Single Responsibility Principle;簡稱 SRP)來解釋好了:由於每個類別的責任都非常明確,而且只扛一個責任,再加上利用介面來降低類別之間的耦合,所以每當程式的需求變動時,就比較容易知道某項功能的變動要改哪一個類別(而不是修改一堆類別)。

此外,若能適當運用 design patterns(如前面的 Decorator 模式),我們甚至可以做到無須修改既有程式碼,就為程式增加新的功能。也就是說,原先寫好的類別會比較「穩定」,不易出現「補東牆卻壞了西牆」的問題。這對降低軟體維護成本來說極為有利。

可測試性

再寫下去就太長了,「可測試性」(testability)的部分就留到下一篇再談吧。
Happy coding :)

0 回應: