2011年10月14日星期五

Dependency Injection 筆記 (1)


我打算將我閱讀 Dependency Injection in .NET 的筆記整理一些出來,這是第一篇。其實,剛開始作筆記的時候,比較像在翻譯。後來陸續加油添醋、畫蛇添足,以及一些刪減取捨,就很難稱得上是翻譯了。故曰「筆記」。

首先要談的是:何謂相依性注入(dependency injection),為什麼要了解它?它對寫程式有什麼幫助?

聲明:筆記內容可能會持續修訂更新,請勿以複製全文的方式轉載。

定義

相依性注入(dependency injection)是一組軟體設計原則與模式,能夠協助我們撰寫寬鬆耦合的程式碼。

方便起見,往後碰到這個名詞時,皆使用英文縮寫:DI。

註:右圖的出處在這裡。寫完之後發現通篇都是文字,有點乏味,故在網路上找了張圖片,純粹為了增添一點趣味,請勿做過多聯想 :p

維護是王

使用 DI 的主要目的,是為了寫出容易維護的程式碼。故 DI 是手段,不是目的。不用因為別人都在談論 DI,就覺得非得在你的程式碼裡面硬生生加入 DI 不可。簡言之,勿為了 DI 而 DI。請牢記:提升可維護性(maintainability)才是終極目標。

如果你的程式碼在完成第一個版本之後就不會再有任何更動,自然可以不用考慮日後維護的問題。但這種情況非常少見。實務上,剛開始看似單純的需求,在實作完第一個版本之後,很快就會出現新的需求或變更既有規格。對付此問題的一個有效方法,便是寬鬆耦合(loose coupling)。那麼,何謂「寬鬆耦合」?

寬鬆耦合

我們知道,在 .NET (或某些物件導向程式語言)的世界裡,任何東西都是「物件」,而應用程式的各項功能便是由各種物件彼此相互合作所達成。在此互動的過程中,這些物件之間勢必會互相牽連,例如:物件 A 呼叫物件 B,B 又去呼叫 C 。這樣的牽連關係,便是耦合。物件之間的關係越緊密,亦即耦合度越高,程式碼就越難維護。因為一旦有任何變動,便容易引發連鎖反應,非得修改多處程式碼不可,導致維護成本大幅升高。故一般而言,在撰寫程式的時候,大都會盡量採寬鬆耦合的方式來設計。

DI 的迷思
  • DI 就是晚期繫結(late binding)或動態繫結
  • DI 只是為了單元測試(unit testing)
  • DI 只是加了威而剛的抽象工廠(Abstract Factory)模式
  • DI 必須搭配 DI 容器(container)。
晚期繫結

晚期繫結就是執行時期才進行繫結,亦即物件之間的連結並非編譯時期決定,而是等到執行時期才知道欲使用之物件的實際型別。舉例來說,瀏覽器的附加元件(add-ins)就是一種晚期繫結的應用。

又如,應用程式所使用的資料庫,如果將來有可能從 SQL Server 換成 Oracle 或其他資料庫,那麼撰寫程式時就得針對這個部分採用比較彈性的設計。常見的作法是,針對一組共同的資料存取介面來撰寫程式。此介面所定義的操作包括開啟與關閉資料庫連線、執行 SQL 查詢、讀取資料等。由於介面只是一份規格,其中並未包含任何實作,因此各項操作會由其他與特定資料庫有關的類別來負責實作。有了中間這層介面,並遵循「針對介面、而非實作來撰寫程式。」(Program to an interface, not an implementation)的原則(註1),應用程式就能與特定的實作類別保持適當距離,不至於綁得太緊。這也就實現了前述所說的寬鬆耦合的精神。

然而,晚期繫結只是 DI 的其中一項好處,而非全部。

註1:Gamma, Erich, et al. Design Patterns. Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994. p. 18。這裡的「實作」,指的是包含實作程式碼的具象類別。
單元測試

有的人認為 DI 主要是為了支援單元測試。這也只是事實的一部份。DI 的確對單元測試有幫助,但就算你不寫單元測試,DI 還是有很有用處。

威而剛版本的抽象工廠

前面提過,物件導向應用程式的各項功能,都是由一群物件彼此合作所共同實現的,而這群物件通常又會依賴底層的架構或框架所提供的基礎服務。也許有人進一步想到,為了達到物件之間的寬鬆藕合,我們可以將物件之間的相依關係移至這層基礎架構(infrastructure)。也就是說,應用程式層次的某類別 A 實際上會使用類別 B1 還是 B2(此二者皆實作了 A 所需服務之介面),係由基礎架構在執行時期來決定。你可以想像得到,這樣的確相當「動態」,也很彈性。於是,基礎架構便得要提供一種能夠在執行時期找到所需類別的服務。這種服務──或者設計模式──有個正式稱呼,叫做「服務定位器」( Service Locator;往後將直接使用英文)。

如此說來,Service Locator 也就是一種更動態、更彈性的物件工廠。這樣說並沒有錯。然而,DI 與 Service Locator 卻是恰恰相反的。DI 並不詢問相依的型別是什麼或在哪裡;它是直接要求呼叫端(用戶端)明確提供相依的物件(如果你對這句話不是很有感覺,等到後面有範例程式碼的時候應該就會明白了)。

DI 容器

認為 DI 必須搭配 Service Locator 模式的人,很可能會認為既然要使用 DI,就一定少不了 DI 容器(DI container),因為 DI 容器通常也扮演了 Service Locator 的角色。

DI 容器的確能夠讓我們更容易以組合元件的方式來建構應用程式,但它並非必要條件。在組合應用程式的時候,如果沒有使用 DI 容器,我們管它叫「窮人的 DI」(poor man's DI)。這種作法雖然需要多花一點點工夫,但除此之外,它並沒有違反任何 DI 設計原則,也沒有犧牲 DI 所提供的任何好處。簡言之,DI  是一組設計模式和原則,DI 容器則是輔助的工具。

下一篇

0 回應: