成功實現依賴注入
我在寫《Dependency Injection in .NET》時經常碰到的一個反應是“你怎么把依賴注入寫成一整本書?”這種難以置信的反應是很自然的,如果你覺得依賴注入的主要模式(構造函數注入)非常容易理解。
雖然這個主要模式易于理解,卻很難成功實現依賴注入,因為這個機制只是一個更大的上下文里的一部分。DI 是對控制反轉(IoC)原則的應用,想要成功實現 IoC,你
private readonly ILog log;public MyConsumer (ILog log)
{
this.log = log ?? LogManager.GetLogger ("My");
}</pre> </div>
就要把你的思維逆轉過來。這篇文章勾畫了成功實現 DI 所需的心智模型。
松散耦合:依賴注入 vs. 服務定位器
如果你不理解 DI 的目的,就很容易把它實現錯。這是我最近看到的一個例子:
從封裝的角度來看,這個方案的主要問題是 MyConsumer 類好像無法確定它是否對日志程序這個依賴的創建擁有控制權。雖然這個示例很簡單,但如果 LogManager 返回的 ILog 實例包裝了一個非托管資源,需要在不使用的時候釋放掉,那就有可能演變成一個問題。
這樣的實現之所以會出現,是因為開發者把全部精力放在讓 MyConsumer 可以進行單元測試。這樣做的理由是開發者只想在單元測試的時候可以替換 ILog,其它情況應該使用 LogManager 返回的實例。
這實際上就是 Bastard Injection 反模式。其中一個潛在問題是它很容易違反里氏代換原則,因為某個特定的實現得到了特殊對待。
DI(Dependency Injection,依賴注入)的目的比起單純地協助進行單元測試要廣泛的多。它的目的是實現松散耦合,以便提升整個解決方案的可維護性。(如果你想知道為什么松散耦合能夠增加可維護性,我的書的第一章討論了這個話題,你可以免費下載試讀。)
松散耦合可被概述為基于接口而不是具體實現進行編程的思想。但是,因為接口沒有構造函數,如何創建那些接口的實例馬上就成了一個問題。
根據你的編程方式,有兩種完全不同的方案可以獲取接口的實例:
對于前面的示例,很容易就會演變成服務定位器(Service Locator)反模式,像這樣:
public MyConsumer (){
<span style="color:#0000ff;">this</span>.log = Locator.Resolve <ilog> ();
}
</ilog></pre>
</div>
除了服務定位器的其它問題,這種方案的問題還在于 LogManager.GetLogger ("My")方法調用所需的參數丟失了。假設其它 consumer 對象需要的日志程序是通過不同的參數實例化的,那么這個版本的服務定位器就無法工作了。
這通常會導致定位器的 Resolve 方法有一個或多個重載版本,以便向服務定位器提供上下文信息。這樣,離違反里氏代換原則就不遠了。
DI 提供了一個更好的方案:
private readonly ILog log;public MyConsumer (ILog log){
<span style="color:#0000ff;">if</span> (log == <span style="color:#0000ff;">null</span>) <span style="color:#0000ff;">throw</span> <span style="color:#0000ff;">new</span> ArgumentNullException (<span style="color:#800000;">"</span><span style="color:#800000;">log</span><span style="color:#800000;">"</span>); <span style="color:#0000ff;">this</span>.log = log;
}</pre> </div>
這是控制反轉的純粹形式。ILog 的任何實現都能接受,同時通過條件語句保證這個實例不為 null。這跟使用服務定位器相反,上下文沒有在構建對象圖的時候丟失。
var consumer = new MyConsumer ( LogManager.GetLogger ("My"));在創建 MyConsumer 的實例時,負責創建的代碼知道這個特定的 consumer 對象使用哪個實現 ILog 接口的類,因此可以根據上下文提供正確的實現。
DI 和服務定位器是實現松散耦合的兩種互斥方案。技術上,兩種都是可行的,但 DI 沒有服務定位器的缺點。
DI 的唯一缺點是它不像服務定位器那樣易于理解。想要成功實現 DI,你需要克服一些障礙。
通往依賴注入的崎嶇之路
學習 DI 的其中一個挑戰,也是你首先碰到的最難的問題:如何獲取一個接口的實例?好消息是一旦你理解構造函數注入只是簡單地通過構造函數請求一個實例,你就跨越了這個最艱難的障礙。
接下來的挑戰比較容易解決,往后一個更加容易。我喜歡把這些挑戰想象成你需要攀越的山。第一個又高又陡,但下一個會比較容易,從那之后很快就會變得平坦:
在成功實現 DI 的路上,你的第一個障礙是理解通過構造函數注入把構建 consumer 對象及其依賴的責任委托給第三方。這個第三方就是對象組合的根(Composition Root),它是應用程序里的一個獨立的點,整個對象圖都是在這里構建的。因為對象組合的根負責構建整個對象圖,所以它掌握了整個上下文,這使它可以對誰依賴誰這個問題做出明智的決定。
這可能嚇跑了一些開發者,因為他們害怕這會導致性能問題,但事實并非如此。
對于大多數人來說,第二個障礙可能是某個依賴的確定需要一個運行時的值。這種情況通常發生在某個依賴要等用戶在用戶界面上做出特定的選擇時才能確定。對于這種情況,抽象工廠通常是一個解決方案。
根據我的經驗,最先的兩個障礙是最難克服的。其它挑戰也會出現,但一般都是個別現象。在我的書的第六章里,我收集了人們可能碰到的常見問題,以及解決它們的辦法。
不管怎樣,一旦你對 DI 形成了正確的心智模型,任何挑戰都能輕易解決的。
關于作者
Mark Seemann 是 AutoFixture 的創作者,也是《Dependency Injection in .NET》的作者。他是一個專業的軟件開發者和架構師,他住在丹麥的哥本哈根,目前是一家丹麥咨詢公司 Commentor 的軟件架構師。他喜歡閱讀、畫畫、彈吉他、好酒以及美食。
查看英文原文:Succeeding with Dependency Injection
來自: InfoQ本文由用戶 openkk 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!