一、定義
依賴倒置原則(Dependence Inversion Principle,DIP)的原始定義為:High level modules shouldnot depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details. Details should depend upon abstractions。
其實裡面包含了三層含義:
- 高層子產品不應該依賴低層子產品,兩者都應該依賴其抽象
- 抽象不應該依賴細節
- 細節應該依賴抽象
- 核心思想:要面向接口程式設計,不要面向實作程式設計。
依賴倒置原則是實作開閉原則的重要途徑之一,它降低了客戶與實作子產品之間的耦合。
由于在軟體設計中,細節具有多變性,而抽象層則相對穩定,是以以抽象為基礎搭建起來的架構要比以細節為基礎 搭建起來的架構要穩定得多。這裡的抽象指的是接口或者抽象類,而細節是指具體的實作類。
使用接口或者抽象類的目的是制定好規範和契約,而不去涉及任何具體的操作,把展現細節的任務交給它們的實作 類去完成。
二、作用
- 依賴倒置原則可以降低類間的耦合性。
- 依賴倒置原則可以提高系統的穩定性
- 依賴倒置原則可以減少并行開發引起的風險。
- 依賴倒置原則可以提高代碼的可讀性和可維護性。
三、舉個例子
我們來舉個例子說明上面的這些作用。(我們反向證明一下)
現在汽車越來越便宜,所有人們出行的時候開車出行的越來越多。在此場景下,有了汽車,開車的人就是司機。
我們使用程式來描述一下:
//奔馳車
public class Benz {
public void run(){
System.out.println("奔馳汽車飛馳-------------");
}
}
//司機
public class Driver {
//司機駕駛汽車
public void drive(Benz benz){
System.out.print("司機開車:");
benz.run();
}
}
//場景類
public class Client {
public static void main(String[] args) {
Driver james=new Driver();
Benz benz=new Benz();
james.drive(benz);
}
}
運作結果:
司機開車:
奔馳汽車飛馳-------------
司機開奔馳車這個項目沒有問題了。業務需求變更的時候更能發覺我們的設計或程式是否是松耦合。是以現在我們 填個需求:司機不僅能開奔馳車,還能開寶馬車,該怎麼實作呢?
不管怎麼實作,先把寶馬車産出來:
//寶馬車
public class BMW {
public void run(){
System.out.println("寶馬車飛馳-----------");
}
}
車有了,可是我們的司機james竟然沒辦法開動,james沒有開動寶馬的方法啊!拿了駕照隻能開奔馳也不太合理 吧!現實世界都不這樣,更何況程式還是對現實世界的抽象呢。
很顯然,我們的設計不合理:司機類和奔馳車類之間是緊耦合的關系,結果就是系統的可維護性降低、可閱讀性降 低,兩個相似的類要閱讀兩個類;這裡增加了一個車類就需要修改司機類,讓程式變得不穩定。這樣我們就證明了 不使用依賴導緻原則就沒有前兩個好處。
繼續證明“減少并行開發的風險”,什麼是并行開發風險?本來隻是一段程式的錯誤或者異常,逐漸波及一個功能、 一個子產品,甚至最後毀掉整個項目。為什麼并行開發有這也的風險?假如一個團隊有20個人,各人負責不同的功能 子產品,A負責汽車類,B負責司機類......在A未完成的情況下,B不能完全的編寫代碼,因為缺少汽車類編譯器編譯根 本通不過,就更不用說單元測試了!在這種不使用依賴倒置原則的情況下,所有的開發都是“單線程”的,隻能是A 做完B再做......在早期的小型項目中還可以,但在現在的中大型項目中就不合适了,需要團隊人員同時并行開發,所 以這個時候依賴原則的作用就展現出來了。因為根據上面的案例已經說明不使用依賴倒置原則就會增加類直接的耦 合性,降低系統的穩定性、可讀性和維護性,增加了并行開發的風險。
咱們将上面的案例引入依賴倒置原則:
//汽車接口
public interface ICar {
void run();
}
//奔馳車
public class Benz implements ICar{
@Override
public void run(){
System.out.println("奔馳汽車飛馳-------------");
}
}
//寶馬車
public class BMW implements ICar{
@Override
public void run(){
System.out.println("寶馬車飛馳-----------");
}
}
//司機接口
public interface IDriver {
//司機駕駛汽車:通過傳入ICar接口實作了抽象之間的依賴關系
void drive(ICar car);
}
//司機類實作司機接口
public class Driver implements IDriver {
//司機駕駛汽車:實作類也傳入ICar接口,至于到底是哪個型号的車,需要在高層子產品中聲明
@Override
public void drive(ICar car){
System.out.print("司機開車:");
car.run();
}
}
//場景類:屬于高層業務邏輯,他對底層的依賴都建立在抽象上
public class Client {
public static void main(String[] args) {
IDriver james=new Driver();
ICar benz=new Benz();
james.drive(benz);
}
}
james和benz表明的類型都是接口,是抽象的,雖然在執行個體化對象的時候調用了低層子產品,但是後續所有操作中, james都是以IDriver類型進行操作,屏蔽了細節對抽象的影響。
如果我們此時再新增一個低層子產品,隻修改業務場景類,也就是高層子產品,對其它低層子產品不需要做任何修改,業 務依然可以運作,把變更引起的風險擴散降到最低。
依賴倒置對并行開發的影響。隻要定義好了接口,即使負責Car開發的程式員工作滞後,我們依然可用進行測試。 引入了JMock工具,根據抽象虛拟一個對象進行測試(不了解該測試工具也沒關系,以後有機會再了解)。
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.integration.junit4.JUnit4Mockery;
import junit.framework.TestCase;
import org.junit.Test;
public class DriverTest extends TestCase {
Mockery context=new JUnit4Mockery();
@Test
public void test1(){
final ICar car=context.mock(ICar.class);
IDriver driver=new Driver();
context.checking(new Expectations(){{
oneOf(car).run();
}
});
driver.drive(car);
}
}
四、實作方法
依賴倒置原則的目的是通過要面向接口的程式設計來降低類間的耦合性,是以我們在實際程式設計中隻要遵循以下5點,就能在項目中滿足這個規則。
- 每個類盡量提供接口或抽象類,或者兩者都具備。
- 變量的聲明類型盡量是接口或者是抽象類。
- 任何類都不應該從具體類派生
- 盡量不要覆寫基類的方法
- 使用繼承時結合裡氏替換原則