2011年10月26日星期三

Dependency Injection 筆記 (3)

前篇,這次要以一個很簡單的入門範例來解說如何在程式中使用 DI。

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

依慣例,我們來看一個 Hello World 版本的 DI 範例。只不過,常見的 Hello World 範例只有短短幾行、甚至只有一行程式碼,但即使是最簡單的 DI 範例,也很難只用一行程式碼來展現其精神。這裡我打算先寫一個非 DI 的例子,再將它改成 DI 的版本。

非 DI 的版本

底下這段程式碼是 Console 應用程式的進入點:

    class Program
    {
        static void Main(string[] args)
        {
            HeavyDuty aTask = new HeavyDuty();
            aTask.Run();
        }
    }

Main 函式會先建立類別 HeavyDuty 的執行個體,然後呼叫該物件的 Run 方法。HeavyDuty 類別的原始碼如下:

    public class HeavyDuty
    {
        private ConsoleLogger logger;
 
        public HeavyDuty()
        {
            // 在建構式裡面就先建立好欲使用的記錄器.
            logger = new ConsoleLogger();
        }
 
        public void Run()
        {
            logger.WriteEntry("HeavyDuty is running...");
        }
    }

從 HeavyDuty 類別的原始碼可以發現,它使用了另一個叫做 ConsoleLogger 的類別來當作記錄器,以便輸出一些訊息。ConsoleLogger 的責任很簡單,就只是將指定的訊息輸出至 console 視窗而已:

    public class ConsoleLogger
    {
        public void WriteEntry(string msg)
        {
            Console.WriteLine(msg);
        }
    }

整理一下:主程式會用到 HeavyDuty 類別來執行某項工作,而 HeavyDuty 又會使用 ConsoleLogger 來輸出 log 訊息。三者關係如下:


這個例子的情境是:應用程式常常會需要寫 log,而寫 log 的機制有好多種,例如本例的 ConsoleLogger 是將 log 訊息輸出至 console 視窗,其他可能的 log 方式還有:寫入 Windows 事件日誌、發送 e-mail、寫入資料庫等等。

問題來了,此例的 ConsoleLogger 是由 HeavyDuty 類別所建立,並非由主程式控制,如果應用程式中還有其他類別需要寫 log,也就必須像 HeaveyDuty 那樣,在類別裡面建立 ConsoleLogger 的物件實體並呼叫其方法。如此一來,若有 N 個類別要寫 log,就有 N 個類別相依於 ConsoleLogger 類別。萬一有一天要改成寫入 Windows 事件日誌(可能會設計另一個 WindowsEventLogger 類別),這要改多少程式碼呀?

接著就來看看 DI 如何處理這個問題。

改成 DI 版本

看過了非 DI 版本的程式寫法以及類別圖,我們知道未來可能會有很多類別會相依於 ConsoleLogger 這個具象類別(concret class),而這層相依性,極可能造成日後很高的維護成本。因此,我們的首要目標就是減輕、甚至消除這層相依性,或者說:解耦合(decouple)。

前兩篇曾提過,介面是解耦合的一種很好用的工具。故我們可以先把「寫入 log」這個操作放到一個介面中,讓所有要寫 log 的類別只針對一個標準介面來操作。就將此介面命名為 ILogger 好了。程式碼很簡單,就只有一個方法:

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

然後,原本的 ConsoleLogger 類別(以及其他要提供寫 log 操作的類別)必須實作此介面:

    public class ConsoleLogger : ILogger
    {
        public void WriteEntry(string msg)
        {
            Console.WriteLine(msg);
        }
    }

接下來,我們希望 HeavyDuty 類別(以及未來其他需要寫 log 的類別)只依賴 ILogger 介面,而不要依賴特定實作。因此,原先在 HeavyDuty 中建立 ConsoleLogger 物件實體的寫法就必須拿掉。可是,要使用物件之前,一定得在某個地方先建立好物件的實體才行啊。那麼,要在哪裡、由誰來建立物件呢?

建立 logger 物件的工作,由於需要用到具象類別,此動作會產生相依性。我們希望將依賴程度盡量降低,因此我們可以將此相依性從 HeavyDuty 類別中抽離,轉移至主程式。換言之,由主程式來建立真正的 logger 物件實體。那麼,HeavyDuty 只要有一個指向實際 logger 物件的參考就夠了--這很簡單,只要主程式在建立 HeavyDuty 物件時,透過建構式的參數傳入 logger 物件參考就解決了。

所以修改後的 HeavyDuty 類別會像這樣:

    public class HeavyDuty
    {
        private ILogger logger;
 
        public HeavyDuty(ILogger aLogger)
        {
            logger = aLogger;
        }
 
        public void Run()
        {
            logger.WriteEntry("HeavyDuty is running...");
        }
    }

有注意到嗎?類別裡面完全沒有指涉任何 logger 類別,而只用到 ILogger 介面而已。真正的物件實體,是透過建構式的參數傳進來。這種透過建構式來提供(注入)相依物件的作法,叫做「建構式注入」(Constructor Injection)。

最後是主程式的 Main 方法:

    class Program
    {
        static void Main(string[] args)
        {
            ILogger logger = new ConsoleLogger();
            HeavyDuty aTask = new HeavyDuty(logger);
            aTask.Run();
        }
    }

到這裡應該完全清楚了。若還覺得有點模糊,可試著倒著順序往回逐一檢視程式碼,應該也能理出一些頭緒。了解各類別之間的關係之後,也就不難整理出底下的類別圖了。


你也可以從這張類別圖,搭配程式碼來推敲各類別的關聯。同時想想看為甚麼要這樣設計,這樣設計有什麼好處(前面都有提到)。

小結

就本文的範例而言,從非 DI 版本改成 DI 版本的寫法有很多種,而作為 Hello World 等級的入門範例,這裡僅採用其中一種最簡單的作法:從類別的建構式傳入實際的物件,藉此將兩個類別之間的相依性抽離至外層(即這裡的 Main 函式)。

希望藉由這個範例的說明,已經有傳達到 DI 的基本精神。
往後還會介紹其他 DI 技法。Stay tune, and happy coding ^_^

2 回應:

真是精彩的系列好文!
非常感謝您的分享。

Thanks! Glad you like it. ^_^