第 1 章:導論

本章從一個基本的問題開始,點出軟體需求變動的常態,以說明為什麼我們需要學習「相依性注入」(Dependency Injection;簡稱 DI)來改善設計的品質。接著以一個簡單的入門範例來比較沒有使用 DI 和改寫成 DI 版本之後的差異,並討論使用 DI 的時機。目的是讓讀者先對相關的基礎概念有個概括的理解,包括可維護性(maintainability)、寬鬆耦合(loose coupling)、控制反轉(inversion of control)、動態繫結、單元測試等等。

 

內容大綱:


為什麼需要 DI?

或許你也曾在某個網路論壇上看過有人提出類似問題:「如何利用字串來來建立物件?」

欲了解此問題的動機與解法,得先來看一下平常的程式寫法可能產生什麼問題。一般來說,建立物件時通常都是用 new 運算子,例如:

ZipCompressor obj = new ZipCompressor();

上面這行程式碼的作用是建立一個 ZipCompressor 物件,或者說,建立 ZipCompressor 類別的執行個體(instance)。從類別名稱不難看出,ZipCompressor 類別會提供壓縮資料的功能,而且是採用 Zip 壓縮演算法。假如有一天,軟體系統已經部署至用戶端,後來卻因為某種原因無法再使用 ZipCompressor 了(例如發現它有嚴重 bug 或授權問題),必須改用別的類別,比如說 RarCompressorGZip。那麼,專案中所有用到 ZipCompressor 的程式碼全都必須修改一遍,並且重新編譯、測試,導致維護成本太高。

為了降低日後的維護成本,我們可以在設計軟體時,針對「將來很可能需要替換的元件」,在程式中預留適度的彈性。簡單地說,就是一種應付未來變化的設計策略。

回到剛才的範例,如果改寫成這樣:

var className = ConfigurationManager.AppSettings["CompressorClassName"];
Type aType = Type.GetType(className);
ICompressor obj = (ICompressor) System.Activator.CreateInstance(aType);

亦即建立物件時,是先從應用程式組態檔中讀取欲使用的類別名稱,然後透過 Activator.CreateInstance 方法來建立該類別的執行個體,並轉型成各壓縮器共同實作的介面 ICompressor。於是,我們就可以將類別名稱寫在組態檔中:

    <appSettings>
        <add key="CompressorClassName" value="MyLib.ZipCompressor, MyLib" />
    <appSettings>

將來若需要換成其他壓縮器,便無須修改和重新編譯程式碼,而只要修改組態檔中的參數值,就能切換程式執行時所使用的類別,進而達到改變應用程式行為的目的。這裡使用了動態繫結的技巧。

 

舉這個例子,重點不在於「以字串來建立物件」的程式技巧,而是想要點出一個問題:當我們在寫程式時,可能因為很習慣使用 new 運算子而不經意地在程式中加入太多緊密的相依關係——即「相依性」(dependency)。進一步說,每當我們在程式中使用 new 來建立第三方(third party)元件的執行個體,我們的程式碼就在編譯時期跟那個類別固定綁(bind)在一起了;這層相依關係有可能是單向依賴,也可能是彼此相互依賴,形成更緊密的「耦合」(coupling),增加日後維護程式的困難。

可維護性

就軟體系統而言,「可維護性」(maintainability)指的是將來修改程式時需要花費的工夫;如果改起來很費勁,我們就說它是很難維護的、可維護性很低的。

有軟體開發實務經驗的人應該會同意:軟體需求的變動幾乎無可避免。如果你的程式碼在完成第一個版本之後就不會再有任何更動,自然可以不用考慮日後維護的問題。但這種情況非常少見。實務上,即使剛開始只是個小型應用程式,將來亦有可能演變成大型的複雜系統;而最初看似單純的需求,在實作完第一個版本之後,很快就會出現新的需求或變更既有規格,而必須修改原先的設計。這樣的修改,往往不只是改動幾個函式或類別而已,還得算上重新跑過一遍完整測試的成本。這就難怪,修改程式所衍生的整體維護成本總是超出預期;這也難怪,軟體系統交付之後,開發團隊都很怕客戶改東改西。

此外,我想大多數人都是比較喜歡寫新程式,享受創新、創造的過程,而少有人喜歡接手維護別人的程式,尤其是難以修改的程式碼。然而,程式碼一旦寫完,某種程度上它就已經算是進入維護模式了1。換言之,程式碼大多是處於維護的狀態。既然如此,身為開發人員,我們都有責任寫出容易維護的程式碼,讓自己和別人的日子好過一些。就如 Damian Conway 在《Perl Best Practices》書中建議的:

寬鬆耦合

在  .NET (或某些物件導向程式語言)的世界裡,任何東西都是「物件」,而應用程式的各項功能便是由各種物件彼此相互合作所達成,例如:物件 A 呼叫物件 B,物件 B 又去呼叫 C。像這樣由類別之間相互呼叫而令彼此有所牽連,便是耦合(coupling)。物件之間的關係越緊密,耦合度即越高,程式碼也就越難維護;因為一旦有任何變動,便容易引發連鎖反應,非得修改多處程式碼不可,導致維護成本提高。

為了提高可維護性,一個常見且有效的策略是採用「寬鬆耦合」(loose coupling),亦即讓應用程式的各部元件適度隔離,不讓它們彼此綁得太緊。一般而言,軟體系統越龐大複雜,就越需要考慮採取寬鬆耦合的設計方式。

當你在設計過程中試著落實寬鬆耦合原則,剛開始可能會看不太出來這樣的設計方式有什麼好處,反而會發現要寫更多程式碼,覺得挺麻煩。但是當你開始維護這些既有的程式,你會發現自己修正臭蟲的速度變快了,而且那些類別都比以往緊密耦合的寫法要來得更容易進行獨立測試。此外,修改程式所導致的「牽一髮動全身」的現象也可能獲得改善,因而降低你對客戶需求變更的恐懼感。

基本上,「可維護性」與「寬鬆耦合」便是我們學習 Dependency Injection 的主要原因。不過,這項技術還有其他附帶好處,例如有助於單元測試與平行開發,這裡也一併討論。

可測試性

當我們說某軟體系統是「可測試的」(testable),指的是有「單元測試」(unit test),而不是類似用滑鼠在視窗或網頁上東點西點那種測試方式。單元測試有「寫一次,不斷重複使用」的優點,而且能夠利用工具來自動執行測試。不過,撰寫單元測試所需要付出的成本也不低,甚至不亞於撰寫應用程式本身。有些方法論特別強調單元測試,例如「測試驅動開發」(Test-Driven Development;TDD),它建議開發人員養成先寫測試的習慣,並盡量擴大單元測試所能涵蓋的範圍,以達到改善軟體品質的目的。

有些情況,要實施「先寫測試」的確有些困難。比如說,有些應用程式是屬於分散式多層架構,其中某些元件或服務需要運行於遠端的數台伺服器上。此時,若為了先寫測試而必須先把這些服務部署到遠端機器上,光是部署的成本與時間可能就讓開發人員打退堂鼓。像這種情況,我們可以先用「測試替身」(test doubles)來暫時充當真正的元件;如此一來,便可以針對個別模組進行單元測試了。Dependency Injection 與寬鬆耦合原則在這裡也能派上用場,協助我們實現測試替身的機制。

平行開發

分而治之,是對付複雜系統的一個有效方法。實務上,軟體系統也常被劃分成多個部分,交給多名團隊成員同時分頭進行各部元件的開發工作,然後持續進行整合,將它們互相銜接起來,成為一個完整的系統。要能夠做到這點,各部分的元件必須事先訂出明確的介面,就好像插座與插頭,將彼此連接的介面規格先訂出來,等到各部分實作完成時,便能順利接上。DI 的一項基本原則就是「針對介面寫程式」(program to an interface),而此特性亦有助於團隊分工合作,平行開發。

了解其優點與目的之後,接著要來談談什麼是 Dependency Injection

什麼是 DI?

如果說「容易維護」是設計軟體時的一個主要品質目標,「寬鬆耦合」是達成此目標的戰略原則,那麼,「相依性注入」(dependency injection;DI)就是屬於戰術層次;它包含一組設計模式與原則,能夠協助我們設計出更容易維護的程式。

DI 經常與「控制反轉」(Inversion of Control;簡稱 IoC)相提並論、交替使用,但兩者並不完全相等。比較精確地說,IoC 涵蓋的範圍比較廣,其中包含 DI,但不只是 DI。換個方式說,DI 其實是 IoC 的一種形式。那麼,IoC 所謂的控制反轉,究竟是什麼意思呢?反轉什麼樣的控制呢?如何反轉?對此問題,我想先引用著名的軟體技術問答網站 Stack Overflow 上面的一個妙答,然後再以範例程式碼來說明。

該帖的問題是:「如何向五歲的孩童解釋 DI?」在眾多回答中,有位名叫 John Munch 的仁兄假設提問者就是那五歲的孩童而給了如下答案:

如此精準到位的比喻,網友一致好評。

 

接下來,依慣例,我打算用一個 Hello World 等級的 DI 入門範例來說明。常見的 Hello World 範例只有短短幾行程式碼,但 DI 沒辦法那麼單純;即使是最簡單的 DI 範例,也很難只用兩三行程式碼來體現其精神。因此,接下來會有更多程式碼和專有名詞,請讀者稍微耐心一點。我們會先實作一個非 DI 的範例程式,然後再將它改成 DI 版本。

入門範例—非 DI 版本

這裡使用的範例情境是應用程式的登入功能必須提供雙因素驗證(two-factor authentication)機制,其登入流程大致有以下幾個步驟:

  1. 使用者輸入帳號密碼之後,系統檢查帳號密碼是否正確。
  2. 帳號密碼無誤,系統會立刻發送一組隨機驗證碼至使用者的信箱。
  3. 使用者收信,獲得驗證碼之後,回到登入頁面繼續輸入驗證碼。
  4. 驗證碼確認無誤,讓使用者登入系統。

依此描述,我們可以設計一個類別來提供雙因素驗證的服務:AuthenticationService。底下是簡化過的程式碼:

class AuthenticationService
{
    private EmailService msgService; 

    public AuthenticationService()
    {
        msgSevice = new EmailService(); // 建立用來發送驗證碼的物件
    }

    public bool TwoFactorLogin(string userId, string pwd)
    {
        // 檢查帳號密碼,若正確,則傳回一個包含使用者資訊的物件。            
        User user = CheckPassword(userId, pwd); 
        if (user != null) 
        {
            // 接著發送驗證碼給使用者,假設隨機產生的驗證碼為 "123456"。
            this.msgService.Send(user, "您的登入驗證碼為 123456");
            return true; 
        }
        return false;
    }
}

AuthenticationService 的建構函式會建立一個 EmailService 物件,用來發送驗證碼。TwoFactorLogin 方法會檢查使用者輸入的帳號密碼,若正確,就呼叫 EmailService 物件的 Send 方法來將驗證碼寄送至使用者的 e-mail 信箱。EmailServiceSend 方法就是單純發送電子郵件而已,這部分的實作細節並不重要,故未列出程式碼;CheckPassword 方法以及 User 類別的程式碼也是基於同樣的理由省略(User 物件會包含使用者的基本聯絡資訊,如 e-mail 位址、手機號碼等等)。

主程式的部分則是利用 AuthenticationService 來處理使用者登入程序。這裡用一個簡化過的 MainApp 類別來表示,程式碼如下,我想應該不用多做解釋。

class MainApp
{
    public void Login(string userId, string password) 
    {
        var authService = new AuthenticationService();
        if (authService.TwoFactorLogin(userId, password))  
        {
            if (authService.VerifyToken("使用者輸入的驗證碼"))
            {
                // 登入成功。
            }
        }
        // 登入失敗。
    }
}

此範例目前涉及四個類別:MainAppAuthenticationServiceUserEmailService。它們的相依關係如圖 1-1 所示,圖中的箭頭代表相依關係的依賴方向。

圖 1-1:類別相依關係圖
圖 1-1:類別相依關係圖
 

透過這張圖可以很容易看出來,代表主程式的 MainApp 類別需要使用 AuthenticationService 提供的驗證服務,而該服務又依賴 UserEmailService 類別。就它們之間的角色關係來說,AuthenticationServiceMainApp 而言是個「服務端」(service)的角色,對於 UserEmailService 而言則是「用戶端」(client;有時也說「呼叫端」)的角色。

目前的設計,基本上可以滿足功能需求。但有個問題:萬一將來使用者想要改用手機簡訊來接收驗證碼,怎麼辦?稍後你會看到,此問題凸顯了目前設計上的一個缺陷:它違反了「開放/封閉原則」。

 

針對「使用者想要改用手機簡訊來接收驗證碼」的這個需求變動,一個天真而快速的解法,是增加一個提供發送簡訊服務的類別:ShortMessageService,然後修改 AuthenticationService,把原本用到 EmailService 的程式碼換成新的類別,像這樣:

class AuthenticationService
{
    private ShortMessageService msgService; 

    public AuthenticationService()
    {
        msgSevice = new ShortMessageService(); // 建立用來發送驗證碼的物件
    }

    public bool TwoFactorLogin(string userId, string pwd)
    {
        // 沒有變動,故省略。
    }
}

其中的 TwoFactorLogin 方法的實作完全沒變,是因為 ShortMessageService 類別也有一個 Send 方法,而且這方法跟 EmailServiceSend 方法長得一模一樣:接受兩個傳入參數,一個是 User 物件,另一個是訊息內容。底下同時列出兩個類別的原始碼。

class EmailService
{
    public void Send(User user, string msg)
    {
        // 寄送電子郵件給指定的 user (略)
    }
}

class ShortMessageService
{
    public void Send(User user, string msg)
    {
        // 發送簡訊給指定的 user (略)
    }
}

你可以看到,這種解法僅僅改動了 AuthenticationService 類別的兩個地方:

  1. 私有成員 msgService 的型別。
  2. 建構函式中,原本是建立 EmailService 物件,現在改為 ShortMessageService

剩下要做的,就只是編譯整個專案,然後部署新版本的應用程式。這種解法的確改得很快,程式碼變動也很少,但是卻沒有解決根本問題。於是,麻煩很快就來了:使用者反映,他們有時想要用 e-mail 接收登入驗證碼,有時想要用手機簡訊。這表示應用程式得在登入畫面中提供一個選項,讓使用者選擇以何種方式接收驗證碼。這也意味著程式內部實作必須要能夠支援執行時期動態切換「提供發送驗證碼服務的類別」。為了達到執行時期動態切換實作類別,相依型別之間的繫結就不能在編譯時期決定,而必須採用動態繫結。

接著就來看看如何運用 DI 來讓剛才的範例程式具備執行時期切換實作類別的能力。

入門範例—DI 版本

為了讓 AuthenticationService 類別能夠在執行時期才決定要使用 EmailService 還是 ShortMessageService 來發送驗證碼,我們必須對這些類別動點小手術,把它們之間原本緊密耦合的關係鬆開——或者說「解耦合」。有一個很有效的工具可以用來解耦合:介面(interface)。

說得更明白些,原本 AuthenticationService 是依賴特定實作類別來發送驗證碼(如 EmailService),現在我們要讓它依賴某個抽象介面,而此介面會定義發送驗證碼的工作必須包含那些操作。由於介面只是一份規格(specification;或者說「合約」),並未包含任何實作,故任何類別只要實作了這份規格,便能夠與 AuthenticationService 銜接,完成發送驗證碼的工作。有了中間這層介面,開發人員便能夠「針對介面、而非針對實作來撰寫程式。」(program to an interface, not an implementation)3,使應用程式中的各部元件保持「有點黏、又不會太黏」的適當距離,從而達成寬鬆耦合的目標。

提煉介面(Extract Interface)

開始動手修改吧!首先要對 EmailServiceShortMessageService 進行抽象化(abstraction),亦即將它們的共通特性抽離出來,放在一個介面中,使這些共通特性成為一份規格,然後再分別由具象類別來實作這份規格。

以下程式碼是重構之後的結果,其中包含一個介面和兩個實作類別。我在個別的 Send 方法中使用 Console.WriteLine 方法來輸出不同的訊息字串,方便觀察實驗結果(此範例是個 Console 類型的應用程式專案)。

// EmailService 和 ShortMessageService 都有 Send 方法,
// 故將此方法提出來,放到一個抽象介面中來定義。
interface IMessageService
{
    void Send(User user, string msg);
}

class EmailService : IMessageService
{
    public void Send(User user, string msg)
    {            
        // 寄送電子郵件給指定的 user (略)
        Console.WriteLine("寄送電子郵件給使用者,訊息內容:" + msg);
    }
}

class ShortMessageService : IMessageService
{
    public void Send(User user, string msg)
    {
        // 發送簡訊給指定的 user (略)
        Console.WriteLine("發送簡訊給使用者,訊息內容:" + msg);
    }
}

看類別圖可能會更清楚些:

圖 1-2:抽離出共通介面之後的類別圖
圖 1-2:抽離出共通介面之後的類別圖
 

介面抽離出來之後,如先前提過的,AuthenticationService 就可以依賴此介面,而不用再依賴特定實作類別。為了方便比對差異,我將修改前後的程式碼都一併列出來:

class AuthenticationService
{
    // 原本是這樣:
    private ShortMessageService msgService; 

    public AuthenticationService()
    {
        this.msgSevice = new ShortMessageService();
    }

    // 現在改成這樣:
    private IMessageService msgService;

    public AuthenticationService(IMessageService service)
    {
        this.msgService = service;
    }
}

修改前後的差異如下:

  • 私有成員 msgService 的型別:修改前是特定類別(EmailServiceShortMessageService),修改後是 IMessageService 介面。
  • 建構函式:修改前是直接建立特定類別的執行個體,並將物件參考(reference)指定給私有成員 msgService; 修改後則需要由外界傳入一個 IMessageService 介面參考,並將此參考指定給私有成員 msgService
控制反轉(IoC)

現在 AuthenticationService 已經不依賴特定實作了,而只依賴 IMessageService 介面。然而,介面只是規格,沒有實作,亦即我們不能這麼寫(無法通過編譯):

IMessageService msgService = new IMessageService(); 
// 錯誤:不能建立抽象介面的執行個體!

那麼物件從何而來呢?答案是由外界透過 AuthenticationService 的建構函式傳進來。請注意這裡有個重要意涵:非 DI 版本的 AuthenticationService 類別使用 new 運算子來建立特定訊息服務的物件,並控制該物件的生命週期;DI 版本的 AuthenticationService 則將此控制權交給外層呼叫端(主程式)來負責——換言之,相依性被移出去了,「控制反轉了」

最後要修改的是主程式(MainApp):

class MainApp
{
    public void Login(string userId, string pwd, string messageServiceType) 
    {
        IMessageService msgService = null;

        // 用字串比對的方式來決定該建立哪一種訊息服務物件。 
        switch (messageServiceType)
        {
            case "EmailService":
                msgService = new EmailService();
                break;
            case "ShortMessageService":
                msgService = new ShortMessageService();
                break;
            default:
                throw new ArgumentException("無效的訊息服務型別!");
        }

        var authService = new AuthenticationService(msgService);  // 注入相依物件
        if (authService.TwoFactorLogin(userId, pwd))  
        {
            // 此處沒有變動,故省略.
        }
    }
}

現在主程式會負責建立訊息服務物件,然後在建立 AuthenticationService 物件時將訊息服務物件傳入其建構函式。這種由呼叫端把相依物件透過建構函式注入至另一個物件的作法是 DI 的一種常見寫法,而這寫法也有個名稱,叫做「建構式注入」(Constructor Injection)。「建構式注入」是實現 DI 的一種方法,第 2 章會進一步介紹。

現在各型別之間的相依關係如下圖所示。請與先前的圖 1-1 比較一下兩者的差異(為了避免圖中箭頭過於複雜交錯,我把無關緊要的配角 User 類別拿掉了) 。

圖 1-3:改成 DI 版本之後的類別相依關係圖
圖 1-3:改成 DI 版本之後的類別相依關係圖
 

你會發現,稍早的圖 1-1 的相依關係,是上層依賴下層的方式;或者說,高層模組依賴低層模組。這只符合了先前提過的 S.O.L.I.D. 五項原則中的「相依反轉原則」(Dependency Inversion Principle;DIP)的其中一小部分的要求。DIP 指的是:

  • 高層模組不應依賴低層模組;他們都應該依賴抽象層(abstractions)。
  • 抽象層不應依賴實作細節;實作細節應該依賴抽象層。

而從圖 1-3 可以發現,DI 版本的範例程式已經符合「相依反轉原則」。其中的 IMessageService 介面即屬於抽象層,而高層模組 AuthenticationService 和低層模組皆依賴中間這個抽象層。

此 DI 版本的範例程式有個好處,即萬一將來使用者又提出新的需求,希望發送驗證碼的方式除了 e-mail 和簡訊之外,還要增加行動裝置平台的訊息推播服務(push notification),以便將驗證碼推送至行動裝置的 app。此時只要加入一個新的類別(可能命名為 PushMessageService),並讓此類別實作 IMessageService,然後稍微改一下 MainApp 便大致完工,AuthenticationService 完全不需要修改。簡單地說,應用程式更容易維護了。

當然,這個範例的程式寫法還是有個缺點:它是用字串比對的方式來決定該建立哪一種訊息服務物件。想像一下,如果欲支援的訊息服務類別有十幾種,那個 switch...case 區塊不顯得太冗長累贅嗎?如果有一個專屬的物件能夠幫我們簡化這些型別對應以及建立物件的工作,那就更理想了。這個部分會在第 3 章〈DI 容器〉中進一步說明。

何時該用 DI?

一旦你開始感受到寬鬆耦合的好處,在設計應用程式時,可能會傾向於讓所有類別之間的耦合都保持寬鬆。換言之,碰到任何需求都想要先定義介面,然後透過 DI 模式(例如先前範例中使用的「建構式注入」)來建立物件之間的相依關係。然而,天下沒有白吃的午餐,寬鬆耦合也不例外。每當你將類別之間的相依關係抽離出來,放到另一個抽象層,再於特定時機注入相依物件,這樣的動作其實多少都會產生一些額外成本。不管三七二十一,將所有物件都設計成可任意替換、隨時插拔,並不總是個好主意。

以  .NET 基礎類別庫(Base Class Library;簡稱 BCL)為例,此類別庫包含許多組件,各組件又包含許多現成的類別,方便我們直接取用。每當你在程式中使用 BCL 的類別,例如 StringDateTimeHashtable 等等,就等於在程式中加入了對這些類別的依賴。此時,你會擔心有一天自己的程式可能需要把這些 BCL 類別替換成別的類別嗎?如果是從網路上找到的開放原始碼呢?答案往往取決於你對於特定類別/元件是否會經常變動的信心程度;而所謂的「經常變動」,也會依應用程式的類型、大小而有不同的定義。

相較於其他在網路上找到或購買的第三方元件,我想多數人都會覺得  .NET BCL 裡面的類別應該會相對穩定得多,亦即不會隨便改來改去,導致既有程式無法編譯或正常執行。這樣的認知,有一部分可能來自於我們對該類別的提供者(微軟)有相當程度的信心,另一部分則是來自以往的經驗。無論如何,在為應用程式加入第三方元件時,最好還是審慎評估之後再做決定。

以下是幾個可能需要使用或了解 DI 技術的場合:

  • 如果你正在設計一套框架(framework)或可重複使用的類別庫,DI 會是很好用的技術。
  • 如果你正在開發應用程式,需要在執行時其動態載入、動態切換某些元件,DI 也能派上用場。
  • 希望自已的程式碼在將來需求變動時,能夠更容易替換掉其中一部份不穩定的元件(例如第三方元件,此時可能搭配 Adapter 模式使用)。
  • 你正在接手維護一個應用程式,想要對程式碼進行重構,以降低對某些元件的依賴,方便測試並且讓程式碼更好維護。

以下是一些可能不適合、或應該更謹慎使用 DI 的場合:

  • 在小型的、需求非常單純的應用程式中使用 DI,恐有殺雞用牛刀之嫌。
  • 在大型且複雜的應用程式中,如果到處都是寬鬆耦合的介面、到處都用 DI 注入相依物件,對於後續接手維護的新進成員來說可能會有點辛苦。在閱讀程式碼的過程中,他可能會因為無法確定某處使用的物件究竟是哪個類別而感到挫折。比如說,看到程式碼中呼叫 IMessageService 介面的 Send 方法,卻沒法追進去該方法的實作來了解接著會發生什麼事,因為介面並沒有提供任何實作。若無人指點、也沒有文件,每次碰到疑問時就只能以單步除錯的方式來了解程式實際運行的邏輯,這確實需要一些耐心。
  • 對老舊程式進行重構時,可能會因為既有設計完全沒考慮寬鬆耦合,使得引入 DI 困難重重。

總之,不加思索地使用任何技術總是不好的;沒有銀彈

本章回顧

就本章的範例而言,從非 DI 版本改成 DI 版本的寫法有很多種,而作為 Hello World 等級的入門範例,這裡僅採用其中一種最簡單的作法:從類別的建構式傳入實際的物件,並透過這種方式,將兩個類別之間的相依性抽離至外層(即範例中的 MainApp 類別),以降低類別之間的耦合度。底下是本章的幾個重點:

  • 可維護性是終極目標,寬鬆耦合是邁向此目標的基本原則,DI 則是協助達成目標的手段。
  • DI 是一組設計原則與模式,其核心概念是「針對介面寫程式,而非針對實作」(program to an interface, not an implementation),並透過將相依關係抽離至抽象層(abstraction layer)來降低各元件之間的耦合度,讓程式更好維護。
  • DI 是 IoC(Inversion of Control;控制反轉)的一種形式,兩者經常交替使用,但並不完全相等。
  • 相依反轉原則(Dependency Inversion Principle;DIP)包含兩個要點:(1) 高層模組不應依賴低層模組,他們都應該依賴抽象層(abstractions); (2) 抽象層不應依賴實作細節,而應該是實作細節依賴抽象層。
  • DI 不是銀彈,使用時仍須思考該用於何處、不該用於何處,以期發揮最大效益。

DI 的內涵包括四大議題:注入方式、物件組合、物件生命週期管理、攔截。本章的範例已稍微觸及前兩項,後續章節會進一步討論這些議題。