深度解讀架構設計中的依賴反轉原則
在日常工作中,總是聽大家說使用依賴反轉原則可以很好地讓業務領域層與基礎設施或框架層進行解耦。即使基礎設施發生變動,也不會影響業務領域代碼。那么架構中所說的依賴反轉原則到底是什么呢?在寫代碼時,我們又該如何實施呢?這節課,我們就來一探究竟。
DIP 原則是什么?
首先,我們來看下架構中的依賴反轉原則是什么。根據維基百科的描述,依賴反轉原則(Dependency inversion principle,DIP)是指一種模塊解耦的實現思想,它使得高層次的模塊不依賴于低層次的模塊的實現細節,從而使得低層次模塊依賴于高層次模塊的抽象。
這一原則的核心思想有如下兩點:
- 高層次的模塊不應該依賴于低層次的模塊,兩者都應該依賴于 抽象接口;
- 抽象接口不應該依賴于具體實現,而具體實現則應該依賴于抽象接口。
接下來,我們通過一個例子來深入理解一下這兩點思想。
當Application模塊需要使用Service模塊提供的服務時,傳統架構上的設計是在Service模塊中定義ComputeService接口以及它的實現類ComputeServiceImpl。
這時Application模塊要想使用Service模塊的ComputeService服務,就需要在Application模塊內引入Service模塊,具體來說,在Java中是需要在Application模塊的POM文件里引入Service模塊的Maven倉庫坐標,然后Application模塊就可以引用到Service模塊的服務了。
設計完畢后,我們會得到兩個意義上的依賴關系。首先,從控制流上(代碼運行時代碼的執行時序上),Application模塊會依賴Service模塊;其次,代碼依賴上,Application模塊也會依賴Service模塊。
由于這種設計會導致Application模塊在源碼上就依賴了Service模塊,所以當Service模塊修改、發布時,Application模塊也需要修改和重新編譯,這顯然不是我們想要的。
而在采用DIP原則的情況下,相同場景下的設計如下圖:
如圖可知,我們把Application模塊所需的ComputeService接口定義到了Application模塊內部,而ComputeService接口的具體實現類ComputeServiceImpl則放到了Service模塊。由于ComputeServiceImpl類需要實現ComputeService接口,所以Service模塊必須在源碼上依賴Application模塊。在Java中則體現為Service模塊的POM文件中要添加Application模塊的maven倉庫坐標,以及ComputeServiceImpl類要使用Application模塊的ComputeService接口代碼。
在基于DIP原則設計完畢后,我們也會得到兩個意義上的依賴關系。首先,從控制流上,Application模塊還是會依賴Service模塊;從代碼依賴上看,Service模塊也依賴Application模塊了。這體現了抽象接口(ComputeService)不應該依賴于具體實現(ComputeServiceImpl),而具體實現類(ComputeServiceImpl)則應該依賴于抽象接口(ComputeService)。另外,這里控制流上的依賴與源碼的依賴關系是相反的,所以叫做依賴反轉。
使用DIP原則,當低層次Service模塊的ComputeServiceImpl類發生改動時,我們可以保證只需要修改和編譯Service模塊就可以了,Application模塊并不會受到影響。當然如果ComputeService接口本身定義發生變化了,還是需要Application模塊進行修改的。因此,請注意,使用DIP原則的前提也是接口是穩定的情況下進行討論的。
如何在代碼層面來實踐 DIP 原則?
下面,我們通過一個代碼示例來看看如何在實踐上基于DIP原則實現上面的場景。
這個Demo分為三個模塊,其中Application模塊、Service模塊分別對應我們前面一直講解的兩個模塊,Main模塊的作用是用來提供main函數入口,用來把Application模塊和Service模塊的功能串起來,實現一個可執行的程序。
三個模塊之間的依賴關系如下圖:
首先,我們來看下Application模塊的內容。
可以看到,Application模塊下有兩個類,其中ComputeService接口定義為:
public interface ComputeService {
int add(int a, int b);
}
ApplicationService的代碼如下:
public class ApplicationService {
private ComputeService computeService;
public ApplicationService(ComputeService computeService) {
this.computeService = computeService;
}
public int add(int a, int b) {
return computeService.add(a, b);
}
}
ApplicationService服務依賴了ComputeService接口來具體實現add操作,這里ComputeService接口就是抽象接口。后面我們會講到,無論Application模塊還是Service模塊,都只會依賴ComputeService這個抽象接口。
我們在 Application模塊目錄下執行mvn clean install命令,就可以把Application模塊安裝到本地Maven倉庫,然后其他模塊就可以通過它的Maven坐標引用這個服務了。
<dependency>
<groupId>org.example</groupId>
<artifactId>Application</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
然后我們看下Service模塊的內容。
Service模塊下只包含ComputeServiceImpl一個類,它的代碼內容如下:
public class ComputeServiceImpl implements ComputeService {
@Override
public int add(int a, int b) {
return a + b;
}
}
可以看到,ComputeServiceImpl類實現了ComputeService抽象接口。由于ComputeServiceImpl類依賴ComputeService接口的源碼,所以我們需要在Service模塊的POM文件里面添加Application模塊的依賴,也就是添加下面的Maven坐標:
<dependency>
<groupId>org.example</groupId>
<artifactId>Application</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
ComputeServiceImpl類的實現比較簡單,就是簡單計算加法,然后返回結果。這里我們需要知道的是,因為ComputeService接口的定義在Application模塊,所以Service模塊在源碼上就依賴了Application模塊。最后在Service模塊的目錄下執行mvn clean install 命令就可以把Service模塊安裝到本地Maven倉庫,然后其他模塊就可以通過其Maven坐標引用這個服務了。
<dependency>
<groupId>org.example</groupId>
<artifactId>Service</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
下面,我們再看Main模塊的內容。
Main模塊內就含有一個叫做Main的類,對應代碼如下:
public class Main {
public static void main(String[] args) {
//1. 創建計算服務
ComputeService computeService = new ComputeServiceImpl();
//2. 創建應用服務
ApplicationService applicationService = new ApplicationService(computeService);
//3. 執行計算,并輸出結果
int result = applicationService.add(1,2);
System.out.println(result);
}
}
代碼1創建了一個ComputeServiceImpl服務的實例,由于ComputeServiceImpl的實現在Service模塊里,所以我們需要在Main組件的POM文件里,填寫下面的Maven坐標:
<dependency>
<groupId>org.example</groupId>
<artifactId>Service</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
代碼2則創建了一個ApplicationService實例,并且把代碼1創建的ComputeServiceImpl實例作為參數。這里我們需要注意的是,ApplicationService的構造函數的入參為ComputeService接口,而不是ComputeServiceImpl。這體現了高層次的Application模塊不應該依賴于低層次的Service模塊(ComputeServiceImpl類),兩者都應該依賴于 抽象接口(ComputeService)。
另外,代碼2由于依賴ApplicationService的源碼,所以Main組件還是需要依賴Application模塊,這就需要在Main組件的POM文件中,添加Application模塊的Maven坐標:
<dependency>
<groupId>org.example</groupId>
<artifactId>Application</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
代碼3就比較簡單了,我們直接調用Application模塊的ApplicationService的add方法執行計算。調用ApplicationService的add方法后,add方法內部則會調用Service模塊的ComputeServiceImpl類的add方法執行具體計算操作。
總結
這次我們先學習了什么是依賴反轉原則。然后,通過一個小場景探討了傳統架構設計中存在的不足,以及如何基于依賴反轉原則來對其進行改進。最后,我們基于Java代碼示例來展示了如何在代碼級別來實施依賴反轉原則。