上回提到,DI 不是終極目標,而是達成目標的方法。DI 有助於降低類別之間的耦合度,而寬鬆耦合又能讓程式碼更好維護。這裡舉例說明之。
聲明:這是我閱讀 Dependency Injection in .NET 一書的筆記,寫作的內容架構與靈感多來自本書。若覺得這些筆記對您有幫助,主要應歸功於該書的作者 Mark Seemann。筆記內容可能會持續修訂更新,請勿以複製全文的方式轉載。
電器與設計模式
日常生活中,四處可見電器用品,例如電視、微波爐、電腦等等。這些電器通常都有條電線,電線尾端是個插頭,而當我們要使用這些電器時,就把插頭插在牆壁或電源插座上,電器便能夠獲得所需之電力。一般情況下,沒有人會捨插座不用,而把電器的電源線直接焊死在牆壁的電源接頭上。假使真的這麼做,那麼萬一有一天電視或電腦故障而需要維修,可就麻煩了。不只電源插座,電腦的 USB 插槽也一樣--它們都具備寬鬆耦合的特性。這裡的電源插座或 USB 插槽,對應到軟體世界裡的概念,便是介面。一個介面就等於是一份規格,而各家廠商所生產的各式各樣的電源插座或 USB 插槽,就是遵照其標準規格(介面)所實作出來的產品,簡稱實作品。用軟體的術語來說,這些實作品就是類別--實作了特定介面的類別。
由此可見,介面的威力即在於一旦訂出標準規格,各家廠商便可針對介面來製作各類產品。對使用者來說,好處則是享有多種選擇,因為他們不會被綁死在特定廠商的產品;只要他們高興,隨時可以更換不同的產品,而且隨插即用!
這就是介面的好處:讓類別與類別之間保持寬鬆耦合,以便提供隨時抽換實作類別的彈性。這種可抽換實作的特性,不僅可以減輕需求變動所產生的風險,甚至能夠應付未來的需求。
Null Object 模式
回到電源插座的例子。如果我們將電腦的電源線從插座上拔起,它們就只是彼此不再連接而已;電腦和插座並不會因此而著火或爆炸。但是在軟體程式的世界裡,若物件 A 會呼叫物件 B(物件 A 依賴物件 B),當你將物件 B 移除,亦即物件 B 不存在時,程式就會發生 NullReferenceException. 類型的錯誤。針對這種狀況,我們可以設計一個空的類別:此類別會實作某個介面,但實作的程式碼完全沒做任何事。這種設計模式叫做 Null Object。此模式的好處是,我們只要先訂出介面,然後用一個空的(不做任何事的)類別來實作此介面,就可以先讓程式正常運作--至少程式執行時不會出現例外(exception)。這有點像是 top-down 的設計方式,因為我們是先把各物件之間的互動過程寫好,然後再逐漸把實作細節補齊。由於物件之間僅持有對方的介面參考(即上一篇提過的 "program to interface"),故在真正的實作品尚未完成之前,我們可以先拿一個空的物件暫時頂著。待實作品完成,便可隨時替換之。
Decorator 模式
一般情況下,如果在使用電腦時突然停電了,尚未儲存的資料就會消失不見。為了解決此問題,我們可以在牆壁的電源插座與電腦電源線之間加入一個不斷電系統(UPS)。此時,UPS 的電源線插頭會插在牆壁的插座上,而電腦的電源則改接在 UPS 上。此三者在串接的時候,都是透過單一的標準介面:插座。這種透過同一介面來串接多個不同物件的作法,叫做 Decorator(裝飾)模式。此模式可以讓我們為既有的物件層層串接新的功能上去,而無須修改既有的程式碼。
Composite 模式
那麼,如果我們希望 UPS 不只接電腦,還要接電風扇、除濕機,這該怎麼辦?
我們可以買一條電源延長線,接在 UPS 上面。如此一來,電風扇、除濕機、和電腦就都可以同時插上延長線的插座了。這裡的電源延長線,就很類似設計模式中的 Composite(複合)模式。因為電源延長線本身又可以再連接其他不同廠牌的延長線(因為插座皆採用相同規格),如此不斷連接下去,就像子目錄裡面又還可以包含層層的子目錄一樣。
呃....延長線的例子有個問題:它看起來更像層層串接,容易和 Decorator 模式搞混。Composite 模式中的物件其實是屬於包含(whole-part)關係,而且物件中包含的物件都是屬於同一族系(有共同的父類別)。因此,用子目錄來類比應該會比較貼切。搭配底下這張圖來看會更清楚:
Adapter 模式
當你的手機沒電,需要充電時,就算有電源延長線也沒用,因為手機充電時所需的電壓並不是一般家庭用電的 110 伏特交流電壓。此時我們通常會使用手機隨附的變壓器(adapter),將變壓器的電源插頭插在牆壁的電源插座,然後將變壓器的另一端連接至手機。像這樣把一種規格(介面)轉換成另一種規格的設計,就叫做 Adapter 模式。
在寫程式時,我們常常會使用別人開發好的元件(稱為 third-party 元件)。可是這些元件可能有部分設計不良,或功能不足,以至於過一段時間之後,會需要替換成其他元件。但問題是,每一種功能相近的元件,其用法可能不盡相同。例如處理壓縮檔案的類別,有的可能會提供 Zip() 和 Unzip() 方法,有的則可能叫作 Compress() 和 Decompress()。為了避免將來更換元件而得修改一堆用戶端程式,我們可以預先寫一個轉換器類別,把實作品(壓縮元件)包在這個類別裡面。這樣做的好處是,用戶端只需要知道轉換器類別怎麼使用就行了,至於這個轉換器類別的 Compress() 和 Decompress() 方法背後是去轉呼叫哪個類別的方法,是屬於實作細節,也是我們所欲隱藏的部分。將來如果要替換壓縮元件(例如改用 7-zip 來壓縮),我們只要修改那個轉換器類別的內部實作細節就行了,用戶端程式碼完全無須更動(因為類別名稱和欲呼叫的方法名稱都沒有變動)。
小結
儘管目前連一行程式碼都還沒看到,但是經過前面幾個例子的說明,你應該可以約略領會寬鬆耦合的優點,以及針對介面來寫程式的威力。
好,針對介面、而不要針對實作類別來寫程式。這道理很簡單。可是,在程式語言的世界裡,介面本身只是個規格,它沒有實作,所以無法建立介面的實體(instance)。例如:
ICompressor aZip = new ICompressor();
這樣是無法通過編譯的。那麼,物件的實體從何而來呢?
這就是 DI(dependency injection)要處理的問題。
下一回
Happy coding :)






.gif)
C# (2009-2011)
4 回應:
老師加油!我都有準時在收看的 :)
Dear 91,
Thank you! ^^
老師您真行!!用簡單易懂的生活常識解釋多種design pattern~以後要把這個部落格加入我的最愛了!!
Dear Nicholas,
Thanks you :)
張貼意見