《.NET 相依性注入》的試閱章節連載 (3)
續上集,本文開始進入第二章。
讀完第 1 章以後,您應該已經了解 DI 的用途與目的,接著要來進一步了解的是 DI 的實作技術,也就是注入相依物件的方式。本章所介紹的相依性注入方式,又稱為「窮人的 DI」(poor man’s DI),因為這些用法都與特定 DI 工具無關,亦即不使用任何現成的 DI 框架(例如 Unity、Autofac)。畢竟,DI 只是一組設計原則與模式,不依賴任何工具也能實現。
設計模式梗概
除了第 1 章提到的 S.O.L.I.D. 設計原則,在運用 DI 技術時,也經常需要搭配一些設計模式(design patterns),例如 Factory Method(工廠方法)、Decorator(裝飾)、Composite(組合)、Adapter(轉換器)等等。基於後續章節討論的必要,本節將介紹幾個相關的設計模式。如需比較完整深入的介紹,可參考相關書籍,例如:《物件導向設計模式》、《深入淺出設計模式》、《重構-向範式前進》等等。
小引-電器與介面
日常生活中,四處可見電器用品,例如電視、微波爐、電腦等等。這些電器通常都有條電線,電線尾端是個插頭,而當我們要使用這些電器時,就把插頭插在牆壁或電源插座上,電器便能夠獲得所需之電力。一般情況下,沒有人會捨插座不用,而把電器的電源線固定焊在牆壁的電源插座。假使真這麼做,萬一有一天電視或電腦故障而需要維修,那可就麻煩了。
不只電源插座,電腦的 USB 插槽也一樣——它們都具備寬鬆耦合的特性。這裡的電源插座或 USB 插槽,對應到軟體世界裡的概念,便是介面。一個介面就等於是一份規格,而各家廠商所生產的各式各樣的電源插座或 USB 插槽,就是遵照其標準規格(介面)所實作出來的產品,或簡稱實作品。用軟體的術語來說,這些實作品就是類別——實作了特定介面的類別。
介面的威力即在於一旦訂出標準規格,各家廠商便可依照標準介面來製作各類產品。對使用者來說,好處則是享有多種選擇,因為他們不會被特定廠商的產品綁住;只要他們高興,隨時可以更換不同的產品,而且通常是隨插即用。在軟體的世界裡,介面也有同樣的好處:讓類別與類別之間保持寬鬆耦合,以便提供隨時抽換實作類別的彈性。
Null Object 模式
回到電源插座的例子。如果我們將電腦的電源線從插座上拔起,它們就只是彼此不再連接而已,電腦和插座並不會因此而著火或爆炸。但是在軟體程式的世界裡,若物件 A 會呼叫物件 B(物件 A 依賴物件 B),而當你將物件 B 移除,亦即物件 B 不存在時,程式就會發生 NullReferenceException 類型的錯誤。於是,我們常常會在程式裡面加入檢查物件參考是否為 null 的邏輯,例如:
如果在程式中一再重複寫這些檢查 null 的邏輯,程式碼便會膨脹,而且在解讀程式的主要邏輯時,常常得要跳過這些檢查邏輯,多少會形成閱讀程式碼的阻礙。針對此問題,我們可以設計一個空的、完全不做任何事的類別,然後在變數有可能是 null 的地方,讓它們指向那個空的物件。這種模式叫做 Null Object。
底下分別是 ILoger 介面以及 NullLogger 和 ConsoleLogger 類別的程式碼:
像底下這個函式,呼叫端只要傳入 ConsoleLogger 物件,日誌訊息就會輸出至 Console;而當呼叫端想要停止記錄日誌,便可傳入 NullLogger 物件。如此一來,就不用在每次寫入日誌訊息時都重複寫一遍檢查 logger 物件是否為 null 的防錯邏輯。
Decorator 模式
一般情況下,如果在使用電腦時突然停電了,尚未儲存的資料就會消失不見。為了解決此問題,我們可以在牆壁的電源插座與電腦電源線之間加入一個不斷電系統(UPS)。此時,UPS 的電源線會接在牆壁的電源插座上,而電腦的電源則改接在 UPS 上。此三者在串接的時候,都是透過單一的標準介面:插座。類似這種透過同一介面來串接多個不同物件的作法,叫做Decorator Pattern(裝飾模式)。此模式可以讓我們為物件層層疊加新的功能上去,而無須修改既有的類別。下圖為 Decorator 模式 的結構圖。
延續前面的 logging 範例,假設想要在每次輸出 log 訊息時額外加上當時的日期時間,而且前提是不可修改既有的 ILogger 和 ConsoleLogger 類別,該怎麼做?
我們可以使用 Decorator 模式。作法為:設計一個新的類別,此類別不僅要實作 ILogger 介面,而且還需要使用既有的 ConsoleLogger 物件來輸出 log 訊息。簡單起見,我就把這個類別命名為 DecoratedLogger。程式碼如下:
下圖描繪了這個簡略版本的 Decorator 模式範例的類別結構:
於是,在用戶端程式中使用這個新的 DecoratedLogger 來輸出 log 訊息時,可以這麼寫:
你可以看到,在這次的修改當中,既有的 ILogger 和 ConsoleLogger 完全沒有動到。我們只增加了一個新類別(DecoratedLogger),就為應用程式加上了新功能。這也就符合了第 1 章提過的 OCP(開放/封閉原則)。
未完待續....
續上集,本文開始進入第二章。
讀完第 1 章以後,您應該已經了解 DI 的用途與目的,接著要來進一步了解的是 DI 的實作技術,也就是注入相依物件的方式。本章所介紹的相依性注入方式,又稱為「窮人的 DI」(poor man’s DI),因為這些用法都與特定 DI 工具無關,亦即不使用任何現成的 DI 框架(例如 Unity、Autofac)。畢竟,DI 只是一組設計原則與模式,不依賴任何工具也能實現。
本章範例原始碼位置:https://github.com/huanlin/di-book-support 裡面的 Examples/ch02 資料夾。
設計模式梗概
每個模式都描述了一個不斷發生在我們周遭的問題,然後描述該問題的核心解法,於是你便可以一再使用該解法,而無須對同樣的事情做兩次工。
—— Christopher Alexander. A Pattern Language.
除了第 1 章提到的 S.O.L.I.D. 設計原則,在運用 DI 技術時,也經常需要搭配一些設計模式(design patterns),例如 Factory Method(工廠方法)、Decorator(裝飾)、Composite(組合)、Adapter(轉換器)等等。基於後續章節討論的必要,本節將介紹幾個相關的設計模式。如需比較完整深入的介紹,可參考相關書籍,例如:《物件導向設計模式》、《深入淺出設計模式》、《重構-向範式前進》等等。
小引-電器與介面
日常生活中,四處可見電器用品,例如電視、微波爐、電腦等等。這些電器通常都有條電線,電線尾端是個插頭,而當我們要使用這些電器時,就把插頭插在牆壁或電源插座上,電器便能夠獲得所需之電力。一般情況下,沒有人會捨插座不用,而把電器的電源線固定焊在牆壁的電源插座。假使真這麼做,萬一有一天電視或電腦故障而需要維修,那可就麻煩了。
不只電源插座,電腦的 USB 插槽也一樣——它們都具備寬鬆耦合的特性。這裡的電源插座或 USB 插槽,對應到軟體世界裡的概念,便是介面。一個介面就等於是一份規格,而各家廠商所生產的各式各樣的電源插座或 USB 插槽,就是遵照其標準規格(介面)所實作出來的產品,或簡稱實作品。用軟體的術語來說,這些實作品就是類別——實作了特定介面的類別。
介面的威力即在於一旦訂出標準規格,各家廠商便可依照標準介面來製作各類產品。對使用者來說,好處則是享有多種選擇,因為他們不會被特定廠商的產品綁住;只要他們高興,隨時可以更換不同的產品,而且通常是隨插即用。在軟體的世界裡,介面也有同樣的好處:讓類別與類別之間保持寬鬆耦合,以便提供隨時抽換實作類別的彈性。
Null Object 模式
回到電源插座的例子。如果我們將電腦的電源線從插座上拔起,它們就只是彼此不再連接而已,電腦和插座並不會因此而著火或爆炸。但是在軟體程式的世界裡,若物件 A 會呼叫物件 B(物件 A 依賴物件 B),而當你將物件 B 移除,亦即物件 B 不存在時,程式就會發生 NullReferenceException 類型的錯誤。於是,我們常常會在程式裡面加入檢查物件參考是否為 null 的邏輯,例如:
if (anObject != null) anObject.DoSomething(); else DoSomethingElse();
如果在程式中一再重複寫這些檢查 null 的邏輯,程式碼便會膨脹,而且在解讀程式的主要邏輯時,常常得要跳過這些檢查邏輯,多少會形成閱讀程式碼的阻礙。針對此問題,我們可以設計一個空的、完全不做任何事的類別,然後在變數有可能是 null 的地方,讓它們指向那個空的物件。這種模式叫做 Null Object。
Null Object 的優點:可減少撰寫判斷物件參考是否為 null 的防錯邏輯。但前提是開發人員得知道有 Null Object 可用,否則還是會寫出多餘的防錯程式碼。Null Object 類別通常要實作某個介面(或繼承自抽象類別),但實作程式碼完全沒做任何事,即所有方法都只是個空殼子,或僅提供無害的預設行為。以程式中常用的 logging(日誌)機制為例,我們可以將寫入日誌的操作定義成一個 ILogger 介面,然後依實際需要實作不同的 logging 類別,例如用來將日誌訊息輸出至 Console 的 ConsoleLogger。此外,考慮到應用程式有時候可能不需要紀錄任何訊息,我們可以實作一個 NullLogger 類別,當作 Null Object 使用。結構圖如下。
底下分別是 ILoger 介面以及 NullLogger 和 ConsoleLogger 類別的程式碼:
public interface ILogger { Log(string msg); } public class NullLogger : ILogger { public void Log(string msg) { // 不做任何事 } } public class ConsoleLogger : ILogger { public void Log(string msg) { Console.WriteLine(msg); } }
像底下這個函式,呼叫端只要傳入 ConsoleLogger 物件,日誌訊息就會輸出至 Console;而當呼叫端想要停止記錄日誌,便可傳入 NullLogger 物件。如此一來,就不用在每次寫入日誌訊息時都重複寫一遍檢查 logger 物件是否為 null 的防錯邏輯。
void DoSomething(ILogger logger) { logger.Log("開始執行 DoSomething 函式。"); .... }
Note: Null Object 本身並不需要「進化」成真正有做事的物件,因為它的存在就是為了提供一個完全不做任何事、不具任何意義的物件。
Decorator 模式
一般情況下,如果在使用電腦時突然停電了,尚未儲存的資料就會消失不見。為了解決此問題,我們可以在牆壁的電源插座與電腦電源線之間加入一個不斷電系統(UPS)。此時,UPS 的電源線會接在牆壁的電源插座上,而電腦的電源則改接在 UPS 上。此三者在串接的時候,都是透過單一的標準介面:插座。類似這種透過同一介面來串接多個不同物件的作法,叫做Decorator Pattern(裝飾模式)。此模式可以讓我們為物件層層疊加新的功能上去,而無須修改既有的類別。下圖為 Decorator 模式 的結構圖。
延續前面的 logging 範例,假設想要在每次輸出 log 訊息時額外加上當時的日期時間,而且前提是不可修改既有的 ILogger 和 ConsoleLogger 類別,該怎麼做?
我們可以使用 Decorator 模式。作法為:設計一個新的類別,此類別不僅要實作 ILogger 介面,而且還需要使用既有的 ConsoleLogger 物件來輸出 log 訊息。簡單起見,我就把這個類別命名為 DecoratedLogger。程式碼如下:
public class DecoratedLogger : ILogger { private ILogger logger; public DecoratedLogger(ILogger aLogger) { logger = aLogger; } public void Log(string msg) { logger.Log(DateTime.Now.ToString() + " - " + msg); } }
下圖描繪了這個簡略版本的 Decorator 模式範例的類別結構:
於是,在用戶端程式中使用這個新的 DecoratedLogger 來輸出 log 訊息時,可以這麼寫:
void DoSomething() { ILogger logger = new DecoratedLogger(new ConsoleLogger()); logger.Log("Hello, 裝飾模式!"); }
你可以看到,在這次的修改當中,既有的 ILogger 和 ConsoleLogger 完全沒有動到。我們只增加了一個新類別(DecoratedLogger),就為應用程式加上了新功能。這也就符合了第 1 章提過的 OCP(開放/封閉原則)。
未完待續....
真是精彩的系列好文!
回覆刪除非常感謝您的分享。
Thanks! Glad you like it. ^_^
回覆刪除具象類別(concret class)英文是不是少一個e? 不好意思,我職業病發作.....
回覆刪除是少了個 e 沒錯。還是一如以往的眼尖啊! 感謝 ^_^
回覆刪除