1. 程式人生 > >[從設計到架構]第四回:依賴的哲學(上)

[從設計到架構]第四回:依賴的哲學(上)

http://www.cnblogs.com/anytao/archive/2008/12/02/1345389.html 

本文將介紹以下內容:

  • 關於依賴和耦合
  • 面向抽象程式設計
  • 依賴倒置原則
  • 控制反轉
  • 依賴注入
  • 工廠模式
  • Unity框架應用
說在,開篇之前
在老子的“小國寡民”論中,提出了一種理想的社會狀態:民至老死,不相往來。這是他老人家的一種社會理想,老死不相往來的人群呈現了一片和諧景象。因為不發生瓜葛,也就無所謂關聯,進而無法倒置衝突。這是先祖哲學中的至純哲理,但理想的大同總是和現實的生態有著或多或少的差距,人類社會無法避免聯絡的發生,所以小國寡民的理想成為一種美麗的夢想,不可實現。同樣的道理,對映到軟體“社會”中,也就是軟體系統結構中,也預示著不同的層次、模組、型別之間也必然存在著或多或少的聯絡,這種聯絡不可避免但可管理。正如人類社會雖然無法實現小國寡民,但是理想的狀態下我們推崇和諧社會,把人群的聯絡由複雜變為簡單,由曲折變為統一,同樣可以使得這種關聯很和諧。所以,軟體系統的使命也應該朝著和諧社會的目標前進,對於不同的關係處理,使用一套行之有效的哲學,把複雜問題簡單化,把僵化問題柔性化,這種哲學或者說方法,在我看來就是:依賴的哲學,也就是本文所要闡釋的中心思想。

1 引言

因為在公司內部進行設計原則和設計模式的培訓,我的第一個任務就是和大家就依賴倒置原則進行溝通。作為5大設計原則之一的DIP原則,單純的由概念而例項在我認為並不能完全闡釋清楚:

  • 什麼是依賴倒置?
  • 為什麼依賴倒置?
  • 如何依賴倒置?
這幾個關鍵的問題,所以我決定不單純的通過DIP而DIP,而是從依賴這個最原始的概念講起,來了解在面向物件軟體設計體系中,關於“關係的處理”,也就是“依賴的哲學”。對,依賴就是關係,處理依賴也就意味著處理關係。因為,我們人類是最善於搞關係的動物,所以原本可以簡單的理論,在人類的意識哲學中變得複雜而多變,以至於我們本應簡單的道理變得如此複雜,這就是依賴。那麼,從依賴講起來了解依賴倒置原則,我覺得首先應該回到以下的問題:
  • 控制反轉、依賴倒置、依賴注入這些概念,你認識但是否熟悉?
  • Unity、ObjectBuilder、Castle這些容器,你相識但是否相知?
  • 面向介面、面向抽象、開放封閉,這些思想,你瞭解但是否瞭然?
帶著對這些問題的思考和思索,Anytao帶領大家就依賴這個話題開始一次循序漸進的面向物件之旅,以解答這些從一開始就有足夠吸引力的問題,從原理到例項,從關係到異同,我期待這篇文章能帶來一些認知的變革。  

2 什麼是依賴,什麼是抽象

2.1 關於依賴和耦合:由小國寡民到和諧社會

在老子的“小國寡民”論中,提出了一種理想的社會狀態:民至老死,不相往來。這是他老人家的一種社會理想,老死不相往來的人群呈現了一片和諧景象。因為不發生瓜葛,也就無所謂關聯,進而無法倒置衝突。這是先祖哲學中的至純哲理,但理想的大同總是和現實的生態有著或多或少的差距,人類社會無法避免聯絡的發生,所以小國寡民的理想成為一種美麗的夢想,不可實現。同樣的道理,對映到軟體“社會”中,也就是軟體系統結構中,也預示著不同的層次、模組、型別之間也必然存在著或多或少的聯絡,這種聯絡不可避免但可管理。正如人類社會雖然無法實現小國寡民,但是理想的狀態下我們推崇和諧社會,把人群的聯絡由複雜變為簡單,由曲折變為統一,同樣可以使得這種關聯很和諧。所以,軟體系統的使命也應該朝著和諧社會的目標前進,對於不同的關係處理,使用一套行之有效的哲學,把複雜問題簡單化,把僵化問題柔性化,這種哲學或者說方法,在我看來就是:依賴的哲學,也就是本文所要闡釋的中心思想。
因為,“耦合是不可避免的”,所以我們首先就從認識依賴和耦合的概念開始,來一步步闡釋我們的依賴哲學思想:

  • 什麼是依賴和耦合

依賴,就是關係,代表了軟體實體之間的聯絡。軟體的實體可能是模組,可能是層次,也可能是具體的型別,不同的實體直接發生依賴,也就意味著發生了耦合。所以,依賴和耦合在我看來是對一個問題的兩種表達,依賴闡釋了耦合本質,而耦合量化了依賴程度。因此,我們對於關係的描述方式,就可以從兩個方面的觀點來分析:


從依賴的角度而言,可以分類為:

    • 無依賴,代表沒有發生任何聯絡,所以二者相互獨立,互不影響,沒有耦合關係。
    • 單向依賴,關係雙方的依賴是單向的,代表了影響的方向也是單向的,其中一個實體發生改變,會對另外的實體產生影響,反之則不然,耦合度不高。
    • 雙向依賴,關係雙方的依賴是相互的,影響也是相互的,耦合度較高。

從耦合的角度而言,可以分類為(此處迴歸到具體的程式碼級耦合概念,以方便概念的闡釋):
    • 零耦合,表示兩個類沒有依賴。
    • 具體耦合,如果一個類持有另一個具體類的引用,那麼這兩個類就發生了具體耦合關係。所以,具體耦合發生在具體類之間的依賴,因此具體類的變更將引起對其關聯類的影響。
    • 抽象耦合,發生在具體類和抽象類的依賴,其最大的作用就是通過對抽象的依賴,應用面向物件的多型機制,實現了靈活的擴充套件性和穩定性。

不同的耦合,代表了依賴程度的差別,我們以“粒度”為概念來分析其耦合的程度。引用中間層來分離耦合,可以使設計更加的優雅,架構更加的柔性,但直接的依賴也存在其市場,過度的設計也並非可取之道。因為效率與效能同樣是設計需要考量的因素,過多的不必要分離會增加呼叫的次數,造成效率浪費。在下文分析依賴倒置原則的弊端之一正是對此問題的進一步闡述。
  • 耦合是如何產生的?

那麼,軟體實體之間的耦合是如何產生呢?迴歸我們每天揮灑的程式碼片段,其實我們在重複的創造著耦合,並且得益於對這種耦合帶來的資料通訊。如果我們將歷史的目光迴歸到軟體設計之初,人類以簡單的機器語言來實現最簡單的邏輯,給一個輸入,實現一個輸出,可以表達為:

隨著軟體世界的革命,業務邏輯的複雜,以上的簡單化處理已經不足以實現更復雜的軟體產品,在系統內部的複雜度成為一個超越人腦可識別的程度時,例如:

 

因此,人類開始發揮重組和簡單化處理的優勢,我們不得不在軟體設計上做出平衡。平衡的結果就是通過對複雜的系統模組化,把複雜問題簡單處理,從而達到能夠被人腦識別的目的。基於這種指導原則,隨著複雜度的增加模組的劃分更加朝著精細化發展,尤其是面向物件程式設計理論的出現,使得對複雜的處理實現了更科學的理論基礎。然而,複雜的問題可以通過劃分實現簡單的功能模組或者技術單元,但由此應運而生的子單元會越來越多,而且越來越多的子單元必須發生資料的通訊才能完成統一的業務處理,所以產生的資料通訊管理也越來越多。對於子單元的管理,也就是我們本文關注的核心概念-依賴,成為新的軟體設計問題,那麼總結前人的經驗,提煉今人的智慧,我們對耦合的產生做以如下歸納:

    • 繼承
    • 聚合
    • 介面
    • 方法呼叫和引用
    • 服務呼叫
瞭解了耦合發生的一般方式,我們就可以進入了核心思想的討論,那就是在認識依賴和了解依賴的基礎上,我們最終追求的目標。
  • 設計的目標:高內聚(High cohesion)、低耦合(Low coupling)
討論了半天,終於是時候對依賴和耦合進行一點兒總結了,也是該進行一點目標訴求了。在軟體設計領域,有那麼幾個至高原則值得我們深刻心中,它們是:
    • 面向抽象程式設計
    • 低耦合,高內聚
    • 封裝變化
    • 實現重用:程式碼重用、演算法重用

對了,就是這些平凡的字眼,彙集了面向物件思想的核心內容,也是本文力求闡釋的禪意心經。關於面向抽象程式設計和封裝變化,我們會在後面詳細闡釋,在此我們需要將注意力關注於“低耦合,高內聚”這一目標。


低耦合,代表了實現最簡單的依賴關係,儘可能的減少類與類、模組與模組、層次與層次、系統與系統之間的聯絡。低耦合,體現了人類追求簡單操作的理想狀態,按照軟體開發的基本實現技巧來追求軟體實體之間的關係簡單化,正是大部分設計模式力圖追求的目標;低耦合,降低了一個類或一個模組發生修改對其他類或模組造成的影響,將影響範圍簡單化。在我們闡釋的依賴關係方式中,實現單向的依賴,實現抽象的耦合,都是實現低耦合的基礎條件。


高內聚,一方面代表了職責的統一管理,一方面體現了關係的有效隔離。例如單一職責原則其實歸根結底是對功能性的一種指導性體現,將功能緊密聯絡的職責封裝為一個類(或模組),而判斷的準則正是基於引起類變化的原因。所以,封裝離不開依賴,而抽象離不開變化,二者的概念和本質都是相對而言的。因此,高內聚的目標體現了以隔離為目標進行統一管理的思想。
那麼,為了達到低耦合、高內聚的目標,通常意義上的設計原則和設計模式其實都是朝著這個方向實現的,因此我們僅僅小結並非普遍意義的規則:

    • 儘可能實現單項依賴
    • 不需要進行資料交換的雙方,不要實現多此一舉的關聯,人們將此形象稱為,不要向陌生人說話(Don't talk to strangers)
    • 保持內部的封裝性,關聯的雙方不要深入實現細節進行通訊,這是保證高內聚的必須條件。

2.2 關於抽象和具體

什麼是抽象呢?我們首先不必澄清什麼是抽象,而從什麼算抽象說起,穩定的、高層的則代表了抽象。就像一個公司,最好保證了高層的穩定,才能保證全域性的發展。在進行系統設計時,穩定的抽象介面和高層邏輯,也代表了整個系統的穩定與柔性。兵熊熊一窩,將良良一窩,系統的邏輯也正如著代表打仗,良好的設計都是自上而下的。而對具體的程式設計實踐而言,介面和抽象類則代表了語言層次的抽象。
追溯概念的分析,我們一一過招,首先來看依賴於具體:

 

因此,為了分離這種緊耦合,最好的辦法就是隔離,引入中間層來分離變化,同時確保中間層本身的穩定性,因此抽象的中間層是最佳的選擇。

 

例如:

    public interface IUserService
    {
    }
    public class UserService : IUserService
    {
    }

下面依賴於具體:

    public class UserManager
    {
        private UserService service = null;
    }

下面依賴於抽象:

    public class UserManager
    {
        private IUserService service = null;
    }

二者的區別僅在於引入了介面IUserService,從而使得UserManager對於UserService的依賴由強減弱。這種方式也在我們的Ezsocio專案中進行service層的設計方式。然而對於依賴的方式並非僅此一種,設計模式中的智慧正是通過各章程式設計技巧進行依賴關係的設計,值得我們關注和學習,本文也在下文進行相關設計模式的討論。
對WCF熟悉的讀者一定不難看出這種實現方式如此類似於WCF的推薦模式,這是契約程式設計的基本思想。關於WCF及SOA的相關內容,本文將在後文進行相關的討論。
總結一番,什麼是抽象,什麼是具體?在我看來,抽象就是系統中對變化封裝的戰略邏輯,體現了系統的必然性和穩定性,能夠被具體層次複用和覆寫;而具體則包含了與具體實現相關的邏輯,體現了系統的動態性和變動性。因此,抽象是穩定的,而具體是變動的。
Bob大叔在《敏捷》一書直言,程式中所有的依賴關係都應終止於抽象類或者介面,就是對面向抽象程式設計一針見血的迴應,其原因歸根結底源自於我們對抽象和具體的認知和分解:關聯應該終止於抽象,而不是具體,保證了系統依賴關係的穩定。具體類發生的修改,不會影響其他模組或者關係。那麼如何做到這種理想的依賴於抽象的設計呢?

  • 層次清晰化
將複雜的問題簡單化,是人類思維的一般智慧,也自然而然是實現軟體設計的基本思路。而將複雜的業務需求通過建模過程的抽象化提煉,去粗取精,去偽存真,凡此種種。而抽象的過程,其目標之一就是形成對於複雜問題簡單化的處理過程,只有形成層次簡單的邏輯才能將複雜需求中的關係梳理清晰,而依賴的本質正如上文所言,不就是處理關係嗎?
所以,清晰的層次劃分,進而形成的模組化,是實現系統抽象的必經之路。
  • 分散集中化
由需求而設計的過程,就是一個分散集中化的過程,把需求相關的業務通過開發流程的需求分析過程進行整理,逐步形成需求規格說明、概要設計和詳細設計等基本流程。分散集中化,是一個梳理需求到形成設計的過程,因此對於把握系統中的抽象和具體而言,是一個重要的分析過程和手段。現代軟體工程已經對此形成了科學的標準化流程處理邏輯,例如可以藉助UML更加清晰的設計流程、分析設計要素,進行標準化溝通和交流。
  • 具體抽象化
將具體問題抽象化,是本節關注的要點,而處理的方法是什麼呢?答案就在設計模式,設計模式是前輩智慧的總結和實踐,所以熟悉和學習設計模式,是學習和實踐設計問題的必經之路。然而,沒有哪個問題是由設計模式全權解決,也沒有那個模式能夠適應所有的問題,因此我們要努力的是儘量積累更多的模式來應對多變的需求。作為軟體設計話題中最重量級的話題,我也會在以後的歲月中對設計模式問題進行一些探討。
  • 封裝變化點
總的來說,抽象和變化就像一對孿生兄弟,將具體的變化點隔離出來以抽象的方式進行封裝,在變化的地方尋找抽象是面對抽象最理想的方式。所以,如何去尋找變化是設計要解決的首頁問題,例如工廠模式的目標是封裝物件建立的變化,橋接模式封裝的是物件間的依賴關係變化等等。23個經典的設計模式,從某種角度來看,正是對不同變化點的封裝角度提出的不同解決方案。
這一設計原則中我們還將之稱為SoC(Separation of Concerns)原則,定義了對於實現理想的高耦合、低內聚目標的統一規則。 

2.3 設計的哲學

之所以花如此篇幅來講述一個看似簡單的問題,其實最終理想是迴歸到軟體設計目標這個命題上。如果悉心鑽研就可發現,設計的最後就是對關係的處理,正如同生活的意義在於對社會的適應一樣。因此,迴歸到設計的目標上我們就可知,完美的設計過程就是對關係的處理過程,也就是對依賴的梳理過程,並最終形成一種合理的耦合結果。
所以,面向物件並不神祕,我們以生活的現實眼光來看更是如此。把面向物件深度濃縮起來,我覺得可以概括為:

  • 目標:重用、擴充套件
  • 核心:低耦合、高內聚
  • 手段:封裝變化
  • 思想:面向介面程式設計、面向抽象程式設計


其實,就是這麼簡單。在這種意義上來說,面向物件思想是現代軟體架構設計的基礎。下面我們以三層架構的設計為例,來進一步感受這種依賴哲學的具體應用。關於依賴的抽象和對變化隔離的基本思路,其實也是實現我們典型三層架構(或者)多層架構的重要基礎。只要使各個層次之間依賴於較穩定的介面,才能使得各個層次之間的變化被隔離在本層之內,不會造成對其他層次的影響,這完全符合開放封閉原則追求的優良設計理念。將這種思路表達為設計,可以表示為:

在此,IDataProvider作為隔離業務層和資料層的抽象,IService作為隔離業務層和表現層的抽象,保證了各個層次的相對穩定和封裝。而體現在此的設計邏輯,就正是我們對於抽象和耦合基本目標概念的體現,例如作為重用的單元,抽象隔離保證了對外發布介面的單一和穩定,所以達到了最高限度的重用;通過引入中間的穩定的介面,達到了不同層次的有效隔離,層與層之間體現為輕度耦合,業務層只持有IDataProvider就可以獲取資料層的所有服務,而表現層也同樣如此;最後,這種方式顯然也直接實踐了面向介面程式設計,面向抽象程式設計的經典理念。
同樣的道理,對於架構設計的很多概念,放大可以擴充套件為面向服務設計所借鑑,放小這正是我們反覆降調的依賴倒置原則在類設計中的基本思想。因此,牢記對我影響至深的一位大牛的說法:軟體設計的任何問題,都可以通過引入中間邏輯瞭解決。而這個中間邏輯,很多時候被封裝為抽象,是最為合理和智慧的解決方案。
讓我們再次高頌《老子》的小國寡民論,來回味關於依賴哲學中,我們如何實現更好的和諧統一,如何遵守科學的軟體管理思想:"鄰國相望,雞犬之聲相聞,民至老死,不相往來。"

3 認識依賴倒置原則(DIP)

3.1 什麼是依賴倒置?

Bob大叔在《Agile Principles, Patterns, and Practices》一書中對依賴倒置原則進行了精闢的總結為:

  • 高層模組不應該依賴於低層模組,二者都應該依賴於抽象。
  • 抽象不應該依賴於具體,細節應該依賴於抽象。
我規規矩矩一字不差的把上述真言放在心裡,卻發現大師的牛論實在有點故作玄虛,就像欣賞Bob在論述DIP時的插畫一樣費解不討好:

 

其實著名的好萊塢原則更形象的闡述了這一思想:你不要調我,我來調你。不管是通俗的還是高尚的,卻都不約而同的揭示了依賴倒置原則的最核心思想就是:

依賴於抽象,對介面程式設計,對抽象程式設計!

相較而言,從實際的生活中來看依賴倒置,就像下面這個示例揭示的一樣。

3.2 從例項開始

綜合對依賴倒置的認識,結合到具體的程式實現而言,依賴倒置預示著程式中的依賴關係不應是具體的型別,而是歸咎於抽象類和介面。下面我們通過一個簡單的例項來分析符合依賴倒置和違反依賴倒置,對於系統設計的影響和區別。我們的需求是為某個遙控器生產商,實現一個萬能遙控器,該遙控器可以對當前市場上的很多電子裝置進行“開啟”和“關閉”的操作,例如你可以使用Anytao牌遙控器開啟海爾電視、創維電視等等,當然更理想的狀態是可以開啟電冰箱、電燈還有門窗等等,總之凡是可以互聯的裝置都是未來萬能遙控器的新需求。
那麼該遙控器廠商在設計之初,該如何去考慮實現一個可以開啟任何裝置的遙控器呢?這一重責首先落在了一位年輕氣盛的小王設計師身上,因為遙控器廠家當前的直接客戶只有海爾電視一家,所以他輕鬆的實現了下面的設計,並且興高采烈的進行了大批量生產:

隨後,廠商多了一個重量級客戶長虹,所以小王不得不對初試設計進行了改造,勉強適應了新的需求,如下:

雖然小王應付了這次需求變動,但是原本的設計顯然已經捉襟見肘。正當小王絞盡腦汁進行改造的同時,新的需求接踵而來:新飛冰箱、飛利浦照明、盼盼防盜門,一個接一個。小王的最終設計變成了這般摸樣:

 

哎,真是太累了。每一次的需求變更都伴隨著小王對遙控器Remote的再次摧殘,Remote內部不斷增加新的引用和操作處理,顯然一個if/else式的判斷佈滿了整個Open和Close的操作中,這種設計顯然無法滿足OCP對擴充套件開放、對修改封閉的要求。顯然,如果想讓賣出去的遙控器也適應新的需求,在小王當前的設計實現方案中是根本無法實現的,遙控器廠商總不能召回已經售出所有的遙控器,再拆開進行重新改造吧。
一籌莫展的小王,終於在崩潰之際想起了經驗豐富的前設計師老王,並立即請教如何解決當前問題的思路。而老王也毫不含糊,給出了一個初步的實現:

在當前的設計中,老王的思路是讓遙控器廠切斷和各個廠家的直接聯絡,而是尋找所有電視廠商的領導(例如,電視機協會),請電視機協會制定所有電視機廠商必須遵守的開啟和關閉等操作的契約,遙控器廠和電視機協會建立直接的聯絡而不是各個具體的電視廠商,於是便有了上述設計思路。而新的需求來臨時,因為各個廠商必須遵守TurnOn和TurnOff的契約,所以輕鬆的萬能遙控器可以應付所有的電視機品牌,實現的具體操作已經由遙控器轉移到具體的廠商手上(順便說說這也是所有權的倒置體現),輕鬆的小王終於大呼一口氣。並且再接再厲修改了更完善的版本:

現在,遙控器基本實現了萬能的要求,任何新的需求或者修改都可以輕鬆勝任。小王終於解決了原本設計的所有問題,帶著感激盛情邀請老王吃飯致謝。席間就坐,小王請教老王二次設計的祕訣,老王神祕一笑沾酒在桌子上寫了幾個大字:依賴倒置。經歷此次設計重構洗禮的小王,也在實戰中體味了設計的精妙,看著依賴倒置幾個字小王也會心的笑了。
萬能遙控器的故事,是一個系統實現中經常的事兒。而這些設計在Ezsocio專案中有廣泛的應用,例如對於DataProvider和Service的處理方式,正是一種典型的遵循DIP原則的設計思路。

3.3 為什麼依賴倒置?

依賴倒置原則揭示了面向物件思想中一個最基本而最核心的話題,那就是:面向抽象程式設計。任何對依賴倒置原則的違反都不同程度的偏離了面向物件設計思想的軌道,所以如果你想自己的程式是否足夠的OO,透徹的瞭解依賴倒置是必不可少的。
所以,要問答為什麼依賴倒置這個話題,我覺得可以從以下幾個方面來闡釋:

  • 依賴倒置是保證開放封閉的前提和基礎。
  • 依賴倒置是對抽象和依賴的基本原則和基本思想的哲學闡釋。
  • 依賴倒置是框架設計的核心思想。
  • 依賴倒置是控制反轉和依賴注入的思想基礎。

綜上而言,依賴倒置是對軟體實體關係處理的基本思想原則,也是其他設計原則與設計模式的基礎之一,因此遵守依賴倒置是實現OO的基本原則,是我們必須瞭解的基礎性原則。下面,我們對此進行詳細的說明和舉例。

3.4 為什麼是倒置?

魯迅先生有云:其實地上本沒有路,走的人多了也便成了路。對依賴倒置原則中的“倒置”二字而言,其實也類似於一條被很多人走過的路,因為習慣性的稱呼走過的為“路”,所以只好把違反習慣的東西稱為“倒置的路”。這倒置的含義,正基於此。
對於從結構化程式設計走過的人來說,基於軟體複用的考慮,側重於對具體模組的複用,因為也就習慣了從高層模組出發了構建系統流程的思維模式,所以那時的高手一出手就實現了高層依賴於底層的典型套路,例如:

高層模組通過自上而下的實現來完成系統功能的呼叫,將這種方式表達為程式碼就是:

        // Release : code01, 2008/11/02                    
        // Author  : Anytao, http://www.anytao.com 
        public static void Main()
        {
            try
            {
                //Do something here.
            }
            catch
            {
                Log(true, "XMLLog");
            }
        }
        public static void Log(bool isRead, string logType)
        {
            if (isRead)
                ReadLog(logType);
            else
                WriteLog(logType);
        }

然而,當軟體設計的模式發展到面向物件階段時,我們發現原來習慣的世界了已經變了。基於高層依賴於底層的弊政,也越來越被可擴充套件性的系統需求折磨的面目全非,例如如果日誌記錄的載體發生變化,當前設計中需要同時自上而下的修改實現的邏輯,同時避免出現越來越多的if/else結構。所以當新的依賴關係從傳統的方式被完全扭轉時,“倒置”二字就此誕生了。我們修改Log實現的設計思路,將可能變化的邏輯封裝為抽象介面,使得高層依賴發生轉換:

 

程式實現的邏輯早已被面向物件的設計思想所取代,我們新的實現變成了:

    // Release : code02, 2008/11/02                    
    // Author  : Anytao, http://www.anytao.com 
    public class Client
    {
        public static void Main()
        {
            ILog myLogger = new XMLLog();
            try
            {
            }
            catch
            {
                myLogger.Write();
            }
        }
    }
    public interface ILog
    {
        void Read();
        void Write();
    }
    public class XMLLog : ILog
    {
        public void Read()
        {
        }
        public void Write()
        { 
        }
    }

所以,瞭解了歷史才能正視現實,對於軟體設計同樣如此,只有認清楚依賴倒置產生的歷史背景,我們才能更加熟練的駕馭倒置含義本身帶來的誤解,而將中心思想牢牢的把握在依賴倒置最核心的設計思想上,那還是萬變不離其宗的:依賴於抽象,這簡單的5個字上。
對於所屬權關係的依賴問題上,我們看到,只有倒置的才是面向物件的,沒有倒置的還是面向結構的。如果你的系統中存在著不合理的依賴關係,那麼依賴倒置將是檢查系統設計最好的標尺,這也是我們把握這一原則的實際意義之一。

3.5 如何依賴倒置?

如何依賴倒置的關鍵,還是體現在如何對抽象和具體的封裝和分離,實踐的基本思路就是封裝變化。這正如我們在單一職責原則中反覆強調,對一個類只有一個引起它變化的原因。我們實踐依賴倒置,仍然可以從關注變化開始,詳細的分析和預測系統中的變化點,然後針對每個可能的變化抽象出相對穩定的約束,這是我們實踐依賴倒置原則最基本的方法步驟。
就原理而言,依賴倒置要求我們的設計:

  • 少繼承,多聚合
  • 單向依賴(低耦合,高內聚)
  • 封裝抽象
  • 對依賴關係都應該終止於抽象類和介面

就實踐而言,經典的軟體設計實踐為我們提出了很多值得借鑑的思路,例如每個設計模式就是對一種特定情況的實踐總結,在此我們繼續列出一些經典的大師忠言,Bob大叔在《Agile Principles, Patterns, and Practices》一書對此進行了3點總結:
  • 任何變數都不應該持有一個指向具體類的指標或者引用。
  • 任何類都不應該從具體類派生。
  • 任何方法都不應該覆寫它的任何基類中的已經實現的方法。
實際上,在實際的設計過程中要完全遵守這幾點要求是有難度的,所以如何既能很好的遵守設計原則,又能很好的適應程式碼情況,是值得權衡的問題,需要我們不斷的積累和實踐。另外,還有幾個經驗只談:
  • 系統架構應該有清晰的層次定義,層次之間通過介面向外提供內聚服務,正如在三層示例中的舉例一樣。
  • 典型的以new進行的物件建立操作,是對依賴倒置原則的典型違反,我們將在後文進行詳細討論。

如何依賴倒置,我們闡釋了一點原則還有一點方法,算是對實現依賴倒置的一點小結。然而,在實際的開發過程中,並沒有一成不變的規則,當前的面嚮物件語言本身就提供了對抽象和封裝的支援,為實現面向物件設計提供了基礎機制。回顧軟體開發的歷史,我們不難看出依賴和封裝哲學的發展軌跡,在結構化程式設計中函式是封裝的基本單元;隨著面向物件的發展C++/C#高階語言以類為基本單元,第一次將資料和行為有機的組合為一個邏輯單元,於是有了對於不同類之間的關係處理哲學;而SOA中封裝的單元上升為service,是一種更高意義的邏輯封裝,實現了更優良的邏輯封裝和鬆散耦合關係。同樣的道理,也體現在三層架構的分割和通訊中,體現在ORM對錶現層和領域層的分離中。
因此,依賴倒置是一種高度的智慧和經驗總結,如何實現依賴倒置也是一種積累和不斷的學習。 

3.6 也有弊端

然而,一味的遵守原則,就等於沒有原則。重要的是,我們需要把握其平衡,在進行開發中適當的把握其程度。Bob在《敏捷》中也提到這個問題,他總結了依賴倒置的兩個弊端,同樣需要我們必要的關注:

  • 對抽象程式設計,需要增加必要的類和輔助程式碼進行支援,某種程度上增加了系統複雜度和維護成本;
  • 當具體類不存在變化時,遵守依賴倒置是多此一舉。所以,如果具體或細節沒有變化可能時,我們沒有必要通過抽象轉嫁依賴是沒有必要的處理。

所以,學習模式或者原則必須把握靈活處理,不能一味強行。