關於 SOLID 原則,我們已經學過單一職責、開閉、里式替換、接口隔離這四個原則。今天,我們再來學習最后一個原則:依賴反轉原則。在前面幾節課中,我們講到,單一職責原則和開閉原則的原理比較簡單,但是,想要在實踐中用好卻比較難。而今天我們要講到的依賴反轉原則正好相反。這個原則用起來比較簡單,但概念理解起來比較難。比如,下面這幾個問題,你看看能否清晰地回答出來:
- “依賴反轉”這個概念指的是“誰跟誰”的“什么依賴”被反轉了?“反轉”兩個字該如何理解?
- 我們還經常聽到另外兩個概念:“控制反轉”和“依賴注入”。這兩個概念跟“依賴反轉”有什么區別和聯系呢?它們說的是同一個事情嗎?
- 如果你熟悉 Java 語言,那 Spring 框架中的 IOC 跟這些概念又有什么關系呢?
看了剛剛這些問題,你是不是有點懵?別擔心,今天我會帶你將這些問題徹底搞個清楚。之后再有人問你,你就能輕松應對。話不多說,現在就讓我們帶着這些問題,正式開始今天的學習吧!
控制反轉(IOC)
在講“依賴反轉原則”之前,我們先講一講“控制反轉”。控制反轉的英文翻譯是 Inversion Of Control,縮寫為 IOC。此處我要強調一下,如果你是 Java 工程師的話,暫時別把這個“IOC”跟 Spring 框架的 IOC 聯系在一起。關於 Spring 的 IOC,我們待會兒還會講到。
我們先通過一個例子來看一下,什么是控制反轉。
public class UserServiceTest {
public static boolean doTest() {
// ...
}
public static void main(String[] args) {//這部分邏輯可以放到框架中
if (doTest()) {
System.out.println("Test succeed.");
} else {
System.out.println("Test failed.");
}
}
}
在上面的代碼中,所有的流程都由程序員來控制。如果我們抽象出一個下面這樣一個框架,我們再來看,如何利用框架來實現同樣的功能。具體的代碼實現如下所示:
public abstract class TestCase {
public void run() {
if (doTest()) {
System.out.println("Test succeed.");
} else {
System.out.println("Test failed.");
}
}
public abstract boolean doTest();
}
public class JunitApplication {
private static final List<TestCase> testCases = new ArrayList<>();
public static void register(TestCase testCase) {
testCases.add(testCase);
}
public static final void main(String[] args) {
for (TestCase case: testCases) {
case.run();
}
}
把這個簡化版本的測試框架引入到工程中之后,我們只需要在框架預留的擴展點,也就是 TestCase 類中的 doTest() 抽象函數中,填充具體的測試代碼就可以實現之前的功能了,完全不需要寫負責執行流程的 main() 函數了。 具體的代碼如下所示:
public class UserServiceTest extends TestCase {
@Override
public boolean doTest() {
// ...
}
}
// 注冊操作還可以通過配置的方式來實現,不需要程序員顯示調用register()
JunitApplication.register(new UserServiceTest();
剛剛舉的這個例子,就是典型的通過框架來實現“控制反轉”的例子。框架提供了一個可擴展的代碼骨架,用來組裝對象、管理整個執行流程。程序員利用框架進行開發的時候,只需要往預留的擴展點上,添加跟自己業務相關的代碼,就可以利用框架來驅動整個程序流程的執行。
這里的“控制”指的是對程序執行流程的控制,而“反轉”指的是在沒有使用框架之前,程序員自己控制整個程序的執行。在使用框架之后,整個程序的執行流程可以通過框架來控制。流程的控制權從程序員“反轉”到了框架。
實際上,實現控制反轉的方法有很多,除了剛才例子中所示的類似於模板設計模式的方法之外,還有馬上要講到的依賴注入等方法,所以,控制反轉並不是一種具體的實現技巧,而是一個比較籠統的設計思想,一般用來指導框架層面的設計。
依賴注入(DI)
接下來,我們再來看依賴注入。依賴注入跟控制反轉恰恰相反,它是一種具體的編碼技巧。依賴注入的英文翻譯是 Dependency Injection,縮寫為 DI。對於這個概念,有一個非常形象的說法,那就是:依賴注入是一個標價 25 美元,實際上只值 5 美分的概念。也就是說,這個概念聽起來很“高大上”,實際上,理解、應用起來非常簡單。
那到底什么是依賴注入呢?我們用一句話來概括就是:不通過 new() 的方式在類內部創建依賴類對象,而是將依賴的類對象在外部創建好之后,通過構造函數、函數參數等方式傳遞(或注入)給類使用。
我們還是通過一個例子來解釋一下。在這個例子中,Notification 類負責消息推送,依賴 MessageSender 類實現推送商品促銷、驗證碼等消息給用戶。我們分別用依賴注入和非依賴注入兩種方式來實現一下。具體的實現代碼如下所示:
// 非依賴注入實現方式
public class Notification {
private MessageSender messageSender;
public Notification() {
this.messageSender = new MessageSender(); //此處有點像hardcode
}
public void sendMessage(String cellphone, String message) {
//...省略校驗邏輯等...
this.messageSender.send(cellphone, message);
}
}
public class MessageSender {
public void send(String cellphone, String message) {
//....
}
}
// 使用Notification
Notification notification = new Notification();
// 依賴注入的實現方式
public class Notification {
private MessageSender messageSender;
// 通過構造函數將messageSender傳遞進來
public Notification(MessageSender messageSender) {
this.messageSender = messageSender;
}
public void sendMessage(String cellphone, String message) {
//...省略校驗邏輯等...
this.messageSender.send(cellphone, message);
}
}
//使用Notification
MessageSender messageSender = new MessageSender();
Notification notification = new Notification(messageSender);
通過依賴注入的方式來將依賴的類對象傳遞進來,這樣就提高了代碼的擴展性,我們可以靈活地替換依賴的類。這一點在我們之前講“開閉原則”的時候也提到過。當然,上面代碼還有繼續優化的空間,我們還可以把 MessageSender 定義成接口,基於接口而非實現編程。改造后的代碼如下所示:
public class Notification {
private MessageSender messageSender;
public Notification(MessageSender messageSender) {
this.messageSender = messageSender;
}
public void sendMessage(String cellphone, String message) {
this.messageSender.send(cellphone, message);
}
}
public interface MessageSender {
void send(String cellphone, String message);
}
// 短信發送類
public class SmsSender implements MessageSender {
@Override
public void send(String cellphone, String message) {
//....
}
}
// 站內信發送類
public class InboxSender implements MessageSender {
@Override
public void send(String cellphone, String message) {
//....
}
}
//使用Notification
MessageSender messageSender = new SmsSender();
Notification notification = new Notification(messageSender);
實際上,你只需要掌握剛剛舉的這個例子,就等於完全掌握了依賴注入。盡管依賴注入非常簡單,但卻非常有用,在后面的章節中,我們會講到,它是編寫可測試性代碼最有效的手段。
依賴注入框架(DI Framework)
弄懂了什么是“依賴注入”,我們再來看一下,什么是“依賴注入框架”。我們還是借用剛剛的例子來解釋。在采用依賴注入實現的 Notification 類中,雖然我們不需要用類似 hard code 的方式,在類內部通過 new 來創建 MessageSender 對象,但是,這個創建對象、組裝(或注入)對象的工作僅僅是被移動到了更上層代碼而已,還是需要我們程序員自己來實現。具體代碼如下所示:
public class Demo {
public static final void main(String args[]) {
MessageSender sender = new SmsSender(); //創建對象
Notification notification = new Notification(sender);//依賴注入
notification.sendMessage("13918942177", "短信驗證碼:2346");
}
}
在實際的軟件開發中,一些項目可能會涉及幾十、上百、甚至幾百個類,類對象的創建和依賴注入會變得非常復雜。如果這部分工作都是靠程序員自己寫代碼來完成,容易出錯且開發成本也比較高。而對象創建和依賴注入的工作,本身跟具體的業務無關,我們完全可以抽象成框架來自動完成。
你可能已經猜到,這個框架就是“依賴注入框架”。我們只需要通過依賴注入框架提供的擴展點,簡單配置一下所有需要創建的類對象、類與類之間的依賴關系,就可以實現由框架來自動創建對象、管理對象的生命周期、依賴注入等原本需要程序員來做的事情。
實際上,現成的依賴注入框架有很多,比如 Google Guice、Java Spring、Pico Container、Butterfly Container 等。不過,如果你熟悉 Java Spring 框架,你可能會說,Spring 框架自己聲稱是控制反轉容器(Inversion Of Control Container)。
實際上,這兩種說法都沒錯。只是控制反轉容器這種表述是一種非常寬泛的描述,DI 依賴注入框架的表述更具體、更有針對性。因為我們前面講到實現控制反轉的方式有很多,除了依賴注入,還有模板模式等,而 Spring 框架的控制反轉主要是通過依賴注入來實現的。不過這點區分並不是很明顯,也不是很重要,你稍微了解一下就可以了。
依賴反轉原則(DIP)
前面講了控制反轉、依賴注入、依賴注入框架,現在,我們來講一講今天的主角:依賴反轉原則。依賴反轉原則的英文翻譯是 Dependency Inversion Principle,縮寫為 DIP。中文翻譯有時候也叫依賴倒置原則。
為了追本溯源,我先給出這條原則最原汁原味的英文描述:
High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.
我們將它翻譯成中文,大概意思就是:高層模塊(high-level modules)不要依賴低層模塊(low-level)。高層模塊和低層模塊應該通過抽象(abstractions)來互相依賴。除此之外,抽象(abstractions)不要依賴具體實現細節(details),具體實現細節(details)依賴抽象(abstractions)。
所謂高層模塊和低層模塊的划分,簡單來說就是,在調用鏈上,調用者屬於高層,被調用者屬於低層。在平時的業務代碼開發中,高層模塊依賴底層模塊是沒有任何問題的。實際上,這條原則主要還是用來指導框架層面的設計,跟前面講到的控制反轉類似。我們拿 Tomcat 這個 Servlet 容器作為例子來解釋一下。
Tomcat 是運行 Java Web 應用程序的容器。我們編寫的 Web 應用程序代碼只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器調用執行。按照之前的划分原則,Tomcat 就是高層模塊,我們編寫的 Web 應用程序代碼就是低層模塊。Tomcat 和應用程序代碼之間並沒有直接的依賴關系,兩者都依賴同一個“抽象”,也就是 Servlet 規范。Servlet 規范不依賴具體的 Tomcat 容器和應用程序的實現細節,而 Tomcat 容器和應用程序依賴 Servlet 規范。
- 控制反轉
實際上,控制反轉是一個比較籠統的設計思想,並不是一種具體的實現方法,一般用來指導框架層面的設計。這里所說的“控制”指的是對程序執行流程的控制,而“反轉”指的是在沒有使用框架之前,程序員自己控制整個程序的執行。在使用框架之后,整個程序的執行流程通過框架來控制。流程的控制權從程序員“反轉”給了框架。 - 依賴注入
依賴注入和控制反轉恰恰相反,它是一種具體的編碼技巧。我們不通過 new 的方式在類內部創建依賴類的對象,而是將依賴的類對象在外部創建好之后,通過構造函數、函數參數等方式傳遞(或注入)給類來使用。 - 依賴注入框架
我們通過依賴注入框架提供的擴展點,簡單配置一下所有需要的類及其類與類之間依賴關系,就可以實現由框架來自動創建對象、管理對象的生命周期、依賴注入等原本需要程序員來做的事情。 - 依賴反轉原則
依賴反轉原則也叫作依賴倒置原則。這條原則跟控制反轉有點類似,主要用來指導框架層面的設計。高層模塊不依賴低層模塊,它們共同依賴同一個抽象。抽象不要依賴具體實現細節,具體實現細節依賴抽象。
“基於接口而非實現編程”與“依賴注入”的聯系是二者都是從外部傳入依賴對象而不是在內部去new一個出來。
區別是“基於接口而非實現編程”強調的是“接口”,強調依賴的對象是接口,而不是具體的實現類;而“依賴注入”不強調這個,類或接口都可以,只要是從外部傳入不是在內部new出來都可以稱為依賴注入。
區別:
1.依賴注入是一種具體編程技巧,關注的是對象創建和類之間關系,目的提高了代碼的擴展性,我們可以靈活地替換依賴的類。
2.基於接口而非實現編程是一種設計原則,關注抽象和實現,上下游調用穩定性,目的是降低耦合性,提高擴展性。
聯系:
都是基於開閉原則思路,提高代碼擴展性!
依賴倒置原則概念是高層次模塊不依賴於低層次模塊。看似在要求高層次模塊,實際上是在規范低層次模塊的設計。
低層次模塊提供的接口要足夠的抽象、通用,在設計時需要考慮高層次模塊的使用種類和場景。
明明是高層次模塊要使用低層次模塊,對低層次模塊有依賴性。現在反而低層次模塊需要根據高層次模塊來設計,出現了「倒置」的顯現。
這樣設計好處有兩點:
- 低層次模塊更加通用,適用性更廣
- 高層次模塊沒有依賴低層次模塊的具體實現,方便低層次模塊的替換
思考題:
基於接口而非實現編程,是一種指導編碼的思想。依賴注入是它的一種具體應用
1⃣️控制反轉是一種編程思想,把控制權交給第三方。依賴注入是實現控制反轉最典型的方法。
2⃣️依賴注入(對象)的方式要采用“基於接口而非實現編程”的原則,說白了就是依賴倒轉。
3⃣️低層的實現要符合里氏替換原則。子類的可替換性,使得父類模塊或依賴於抽象的高層模塊無需修改,實現程序的可擴展性。