软件设计原则(四)依赖倒置原则 -Dependence Inversion Principle
依賴倒轉原則就是要依賴于抽象,不要依賴于實現。(Abstractions should not depend upon details. Details should depend upon abstractions.)要針對接口編程,不要針對實現編程。(Program to an interface, not an implementation.)也就是說應當使用接口和抽象類進行變量類型聲明、參數類型聲明、方法返還類型說明,以及數據類型的轉換等。而不要用具體類進行變量的類型聲明、參數類型聲明、方法返還類型說明,以及數據類型的轉換等。要保證做到這一點,一個具體類應當只實現接口和抽象類中聲明過的方法,而不要給出多余的方法。
傳統的過程性系統的設計辦法傾向于使高層次的模塊依賴于低層次的模塊,抽象層次依賴于具體層次。倒轉原則就是把這個錯誤的依賴關系倒轉過來。面向對象設計的重要原則是創建抽象化,并且從抽象化導出具體化,具體化給出不同的實現。繼承關系就是一種從抽象化到具體化的導出。抽象層包含的應該是應用系統的商務邏輯和宏觀的、對整個系統來說重要的戰略性決定,是必然性的體現。具體層次含有的是一些次要的與實現有關的算法和邏輯,以及戰術性的決定,帶有相當大的偶然性選擇。具體層次的代碼是經常變動的,不能避免出現錯誤。
從復用的角度來說,高層次的模塊是應當復用的,而且是復用的重點,因為它含有一個應用系統最重要的宏觀商務邏輯,是較為穩定的。而在傳統的過程性設計中,復用則側重于具體層次模塊的復用。依賴倒轉原則則是對傳統的過程性設計方法的“倒轉”,是高層次模塊復用及其可維護性的有效規范。
特例:對象的創建過程是違背“開—閉”原則以及依賴倒轉原則的,但通過工廠模式,能很好地解決對象創建過程中的依賴倒轉問題。
2.關系
“開-閉”原則與依賴倒轉原則是目標和手段的關系。如果說開閉原則是目標,依賴倒轉原則是到達"開閉"原則的手段。如果要達到最好的"開閉"原則,就要盡量的遵守依賴倒轉原則,依賴倒轉原則是對"抽象化"的最好規范。
里氏代換原則是依賴倒轉原則的基礎,依賴倒轉原則是里氏代換原則的重要補充。
3.耦合(依賴)關系的種類
零耦合(Nil Coupling)關系:兩個類沒有耦合關系
具體耦合(Concrete Coupling)關系:發生在兩個具體的(可實例化的)類之間,經由一個類對另一個具體類的直接引用造成。
抽象耦合(Abstract Coupling)關系:發生在一個具體類和一個抽象類(或接口)之間,使兩個必須發生關系的類之間存有最大的靈活性。
3.1如何把握耦合
我們應該盡可能的避免實現繼承,原因如下:
1 失去靈活性,使用具體類會給底層的修改帶來麻煩。
2 耦合問題,耦合是指兩個實體相互依賴于對方的一個量度。程序員每天都在(有意識地或者無意識地)做出影響耦合的決定:類耦合、API耦合、應用程序耦合等等。在一個用擴展的繼承實現系統中,派生類是非常緊密的與基類耦合,而且這種緊密的連接可能是被不期望的。如B extends A ,當B不全用A中的所有methods時,這時候,B調用的方法可能會產生錯誤!
我們必須客觀的評價耦合度,系統之間不可能總是松耦合的,那樣肯定什么也做不了。
3.2我們決定耦合程度的依據是什么
簡單的說,就是根據需求的穩定性,來決定耦合的程度。對于穩定性高的需求,不容易發生變化的需求,我們完全可以把各類設計成緊耦合的(我們雖然討論類之間的耦合度,但其實功能塊、模塊、包之間的耦合度也是一樣的),因為這樣可以提高效率,而且我們還可以使用一些更好的技術來提高效率或簡化代碼,例如c# 中的內部類技術。可是,如果需求極有可能變化,我們就需要充分的考慮類之間的耦合問題,我們可以想出各種各樣的辦法來降低耦合程度,但是歸納起來,不外乎增加抽象的層次來隔離不同的類,這個抽象層次可以是抽象的類、具體的類,也可以是接口,或是一組的類。我們可以用一句話來概括降低耦合度的思想:"針對接口編程,而不是針對實現編程。
在我們進行編碼的時候,都會留下我們的指紋,如public的多少,代碼的格式等等。我們可以耦合度量評估重新構建代碼的風險。因為重新構建實際上是維護編碼的一種形式,維護中遇到的那些麻煩事在重新構建時同樣會遇到。我們知道在重新構建之后,最常見的隨機bug大部分都是不當耦合造成的 。
如果不穩定因素越大,它的耦合度也就越大。
某類的不穩定因素=依賴的類個數/被依賴的類個數
依賴的類個數= 在編譯此類的時被編譯的其它類的個數總和
3.3怎樣將大系統拆分成效系統
解決這個問題的一個思路是將許多類集合成一個更高層次的單位,形成一個高內聚、低耦合的類的集合,這是我們設計過程中應該著重考慮的問題!
耦合的目標是維護依賴的單向性,有時我們也會需要使用壞的耦合。在這種情況下,應當小心記錄下原因,以幫助日后該代碼的用戶了解使用耦合真正的原因。
4.怎樣做到依賴倒轉
以抽象方式耦合是依賴倒轉原則的關鍵。抽象耦合關系總要涉及具體類從抽象類繼承,并且需要保證在任何引用到基類的地方都可以改換成其子類,因此,里氏代換原則是依賴倒轉原則的基礎。
在抽象層次上的耦合雖然有靈活性,但也帶來了額外的復雜性,如果一個具體類發生變化的可能性非常小,那么抽象耦合能發揮的好處便十分有限,這時可以用具體耦合反而會更好。
層次化:所有結構良好的面向對象構架都具有清晰的層次定義,每個層次通過一個定義良好的、受控的接口向外提供一組內聚的服務。
依賴于抽象:建議不依賴于具體類,即程序中所有的依賴關系都應該終止于抽象類或者接口。盡量做到:
1、任何變量都不應該持有一個指向具體類的指針或者引用。
2、任何類都不應該從具體類派生。
3、任何方法都不應該覆寫它的任何基類中的已經實現的方法。
5.依賴倒轉原則的優缺點
依賴倒轉原則雖然很強大,但卻最不容易實現。因為依賴倒轉的緣故,對象的創建很可能要使用對象工廠,以避免對具體類的直接引用,此原則的使用可能還會導致產生大量的類,對不熟悉面向對象技術的工程師來說,維護這樣的系統需要較好地理解面向對象設計。
依賴倒轉原則假定所有的具體類都是會變化的,這也不總是正確。有一些具體類可能是相當穩定,不會變化的,使用這個具體類實例的應用完全可以依賴于這個具體類型,而不必為此創建一個抽象類型。
DIP,Dependence Inversion Principle:
High level modules should not depend upon low level modules. Both should depend upon abstractions.
Abstractions should not depend upon details. Details should depend upon abstractions.
即“面向接口編程”:
- 高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象;——模塊間的依賴通過抽象發生。實現類之間不發生直接的依賴關系(eg. 類B被用作類A的方法中的參數),其依賴關系是通過接口或抽象類產生的;
- 抽象不應該依賴細節;——接口或抽象類不依賴于實現類;
- 細節應該依賴抽象;——實現類依賴接口或抽象類。
何為“倒置”?
??????? 依賴正置:類間的依賴是實實在在的實現類間的依賴,即面向實現編程,這是正常人的思維方式;
??????? 而依賴倒置是對現實世界進行抽象,產生了抽象間的依賴,代替了人們傳統思維中的事物間的依賴。
依賴倒置可以減少類間的耦合性、降低并行開發引起的風險。
示例(減少類間的耦合性):
??????? 例如有一個Driver,可以駕駛Benz:
??????? 問題來了:現在有變更,Driver不僅要駕駛Benz,還需要駕駛BMW,怎么辦?
??????? Driver和Benz是緊耦合的,導致可維護性大大降低、穩定性大大降低(增加一個車就需要修改Driver,Driver是不穩定的)。
示例(降低并行開發風險性):
??????? 如上例,Benz類沒開發完成前,Driver是不能編譯的!不能并行開發!
問題由來:
??????? 類A直接依賴類B,假如要將類A改為依賴類C,則必須通過修改類A的代碼來達成。這種場景下,類A一般是高層模塊,負責復雜的業務邏輯;類B和類C是低層模塊,負責基本的原子操作;假如修改類A,會給程序帶來不必要的風險。
????????解決辦法:
??????? 將類A修改為依賴接口I,類B和類C各自實現接口I,類A通過接口I間接與類B或者類C發生聯系,則會大大降低修改類A的幾率。
??????? 上例中,新增一個抽象ICar接口,ICar不依賴于BMW和Benz兩個實現類(抽象不依賴于細節)。
??????? 1)Driver和ICar實現類松耦合
??????? 2)接口定下來,Driver和BMW就可獨立開發了,并可獨立地進行單元測試
??????? 依賴有三種寫法:
?????? 1)構造函數傳遞依賴對象(構造函數注入)
????? 2)setter方法傳遞依賴對象(setter依賴注入)
????? 3)接口聲明依賴對象(接口注入)
建議:
??????? DIP的核心是面向接口編程;DIP的本質是通過抽象(接口、抽象類)使各個類或模塊的實現彼此獨立,不互相影響。
在項目中遵循以下原則:
總結
以上是生活随笔為你收集整理的软件设计原则(四)依赖倒置原则 -Dependence Inversion Principle的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 软件设计原则(三)里氏替换原则 -Lis
- 下一篇: 软件设计原则(五)接口隔离原则 -Int