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