前言
今天學習 SOLID 中的最后一個原則,依賴反轉原則。
本章內容,可以帶著如下幾個問題:
- “依賴反轉” 這個概念指的是 “誰跟誰” 的 “什么依賴” 被反轉了? “反轉” 這兩個字該如何理解。
- 我們還經常聽到另外兩個概念:“控制反轉” 和 “依賴注入”。這兩個概念跟 “依賴反轉” 有什么區別和聯系嗎?它們說的是同一個事情嗎?
- 如果你熟悉 Java 語言,那 Spring 框架中的 IOC 跟這些概念有什么關系?
控制反轉(IOC)
在講依賴反轉原則之前,我們先講下“控制反轉”。
如果你是Java工程師的話,暫時別把這個 “IOC” 和 Spring 框架的 “IOC” 聯系在一起。關于 Spring 的 IOC ,我們待會還會講到。
先通過一個例子來查看,什么是控制反轉。
public class UserServiceTest {public static final boolean doTest() {// ...}public static void main(String[] args) { // 這部分邏輯可以放到框架中if (doTest()) {System.out.println("Test succeeded.");} else {System.out.println("Test failed.");}}
}
在上面的代碼中,所有的流程都由程序員來控制。如果我們抽象出下面這樣一個框架,再來看,如何利用框架來實現同樣的功能。
public abstract class TestCase {public void run() {if (doTest()) {System.out.println("Test succeeded.");} else {System.out.println("Test failed.");}}public abstract void 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 testCase : testCases) {testCase.run();}}
}
這個簡化版本的測試框架引入到工程之后,我們只需要在框架預留的擴展點,即 TestCase
類中的 doTest()
抽象函數中,填充具體的測試代碼就可以實現之前的功能了,完全不需要寫負責執行流程的 main()
函數了。具體代碼如下所示:
public class UserServiceTest extends TestCase {@Overridepublic void doTest() {// ...}
}// 注冊操作還可以通過配置的方式來實現,不需要程序員顯示調用 register()
JunitApplication.register(new UserServiceTest());
上面例子,就是典型的通過框架來實現“控制反轉”的例子。框架提供了一個可擴展的代碼骨架,用來封裝對象、管理整個執行流程。程序員利用框架進行開發的時候,只需要往預留的擴展點上,添加跟自己業務相關的代碼,就可以利用框架驅動整個程序流程的執行。
- 這里的“控制” 指的是對程序執行流程的控制。
- 而“反轉”是指流程的控制權從程序員“反轉”到了框架。在沒有使用框架前,程序員自己控制整個程序的執行;使用框架后,整個執行流程可以通過框架來控制。
實際上,實現控制反轉的方法有很多,除了上面的例子中類似模板設計模式的方法之外,還有依賴注入等方法。所以,控制反轉并不是一種具體的實現技巧,而是一種比較籠統的設計思想,一般用來指導框架層面的設計。
依賴注入(DI)
依賴注入和控制反轉相反,它是一種編碼技巧。
依賴注入英文翻譯為:Dependency Injection,縮寫為 DI。
什么是依賴注入? 用一句話來概括就是:不通過 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.sendMessage(cellPhone, message);}
}public class MessageSender {public void sendMessage(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.sendMessage(cellPhone, message);}
}public class MessageSender {public void sendMessage(String cellPhone, String 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.sendMessage(cellPhone, message);}
}public interface MessageSender {void sendMessage(String cellPhone, String message);
}public class SmsSender implements MessageSender {@Overridepublic void sendMessage(String cellPhone, String message) { /**/ }
}public class InboxSender implements MessageSender {@Overridepublic void sendMessage(String cellPhone, String message) { /**/ }
}// 使用Notification
MessageSender messageSender = new SmsSender();
Notification notification = new Notification(messageSender);
實際上,只需掌握剛剛舉的例子,就掌握了依賴注入。盡管依賴注入非常簡單,但卻非常有用。
依賴注入框架(DI Framework)
弄懂了什么是“依賴注入”,在來看下,什么“依賴注入框架”。還是借用剛剛例子來解釋。
在采用依賴注入實現的 Notification
類中,雖然不需要使用類似 hard code 的方式,在類內部通過 new 來創建 MessageHandler
對象,但是這個創建對象、組裝(或注入)對象的工作,僅僅是被移動到了上層代碼而已,還是需要我們程序員自己來實現。具體代碼如下:
public class Demo {public static void main(String[] args) {MessageSender messageSender = new SmsSender(); // 創建對象Notification notification = new Notification(messageSender); // 依賴注入notification.sendMessage("13910221123", "短信驗證碼:2345");}
}
在實際開發中,一些項目可能會涉及幾十、上百、甚至幾百個類,類對象的創建的依賴注入會變得非常復雜。如果這部分工作由程序員自己寫代碼來完成,容易出錯且開發成本比較高。而創建和依賴注入的工作,本身和業務無關,完全可以抽象成框架來自動完成。
這個框架就是“依賴注入框架”。只需要通過依賴注入框架提供的擴展點,簡單配置以下所有需要創建的類對象、類與類之間的依賴關系,就可以實現由框架來自動創建對象、管理對象的生命這周期、依賴注入等事情。
實際上,現成的依賴注入框架有很多,比如 Google Guice、Java Spring、Pico Container 等。
不過,Spring 框架自己聲稱是控制反轉容器(Inversion of Control Container)。
實際上,這兩種說法都沒錯。只是控制反轉容器這種表述是一種非常寬泛的描述,除了依賴注入,還有模板模式等,而 Spring 框架的控制反轉主要是通過依賴注入來實現的。
依賴反轉原則(DIP)
接下來講一下本章的主角:依賴反轉原則。依賴反轉原則的英文翻譯是 Dependency Inversion Principle,縮寫為 DIP。有時也翻譯成依賴倒置原則。
英文原文描述:
High-level modules shouldn’t depend on low-level modules。Both modules should depend on abstractions shouldn’t depend on details。Details depend on abstractions.
翻譯成中文,大概意思是: 高層模塊不要依賴低層模塊。高層模塊和低層模塊應該通過抽象來相互依賴。此外,抽象不要依賴具體實現細節,具體實現細節依賴抽象。
所謂高層模塊和低層模塊的劃分,簡單來說,在調用鏈上,調用者屬于高層,被調用者屬于低層。在平時的業務代碼開發中,高層模塊依賴底層模型是沒有任何問題的。實際上,這條原則主要用來指導框架層面的設計,跟前面講到的控制反轉類似。我們拿 Tomcat 這個 Servlet 容器作為例子來解釋下。
Tomcat 是運行 Java Web 應用程序的容器。我們編寫 Web 應用程序代碼只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器調用執行。
- 按照之前的劃分原則,Tomcat 就是高層模塊,我們編寫的 Web 應用程序屬于低層模塊。
- Tomcat 和 應用程序之間并沒有直接的依賴關系,兩者依賴同一個抽象,也就是 Servlet 規范。
- Servlet 規范不依賴具體的 Tomcat 容器和應用程序的實現細節。
- 而 Tomcat 容器和應用程序依賴 Servlet 規范。
總結
1.控制反轉
控制反轉是一個比較抽象的設計思想,并不是具體的實現方法,一般指導框架層面的設計。
- “控制” 指的是對程序執行流程的控制。
- “反轉” 指的是在沒有使用框架之前,程序員自己控制整個程序的執行。在使用框架之后,整個程序的執行流程通過框架來控制。流程控制權從程序員 “反轉” 給了框架。
2.依賴注入
依賴注入和控制反轉相反,它是一種具體的編碼技巧。我們不通過 new 的方式在類內部創建依賴類的對象,而是將依賴類的對象在外部創建好之后,通過構造函數、函數參數等方式注入給類適用。
3.依賴注入框架
我們通過依賴注入框架提供的擴展點,簡單配置下需要的類及類與類之間的依賴關系,就可以實現由框架來自動創建對象、管理對象生命周期、依賴注入等原本需要程序員來做的事情。
4.依賴反轉原則
依賴反轉原則,也叫依賴倒置原則。這條原則跟控制反轉有點類似,主要用來指導框架層面的設計。
- 高層模型不依賴低層模塊
- 它們共同依賴同一個抽象
- 抽象不要依賴具體實現細節
- 具體實現細節依賴抽象。