文章目錄
- 依賴反轉原則
- 控制反轉
- 怎麼了解“反轉”?
- 控制反轉的好處
- 控制反轉執行個體
- 依賴注入
- 依賴注入執行個體
- 依賴注入執行個體2
- 參考資料
依賴反轉原則
依賴反轉原則:高層子產品(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 規範。
再舉一個例子:JDBC其實也是一種DIP原則(依賴反轉原則),各個資料庫廠商自行實作驅動,實作高層子產品和低層子產品的劃分。
我們使用spring架構的小夥伴,相信都知道“控制反轉”、“依賴注入”,也許很多小夥伴知其然并不知其是以然,今天我們就先聊聊,到底什麼是控制反轉、依賴反轉、依賴注入?
控制反轉
實際上,控制反轉是一個比較籠統的設計思想,并不是一種具體的實作方法,一般用來指導架構層面的設計。這裡所說的“控制”指的是對程式執行流程的控制,而“反轉”指的是在沒有使用架構之前,程式員自己控制整個程式的執行。在使用架構之後,整個程式的執行流程通過架構來控制。流程的控制權從程式員“反轉”給了架構。
怎麼了解“反轉”?
相對于傳統的面向過程程式設計實踐而言
- 程式的流程控制權發生了轉變
- 應用程式與第三方代碼之間的調用關系發生了轉變
- 反轉前:我們自己的代碼決定程式的工作流程,并調用第三方代碼(我們自己的代碼是甲方,第三方代碼是乙方)
- 反轉後:第三方代碼(架構)決定程式的工作流程,并調用我們寫的代碼(我們自己的代碼是乙方,第三方代碼是甲方)
控制反轉的好處
好處也是很直接的,那就是複用。
複用代碼有三種方式:類庫、架構、設計模式。
-
類庫:強調代碼複用;
定義一組可複用的代碼,供其他程式調用——拿來主義,别人的東西拿來用,用别人的錘子砸核桃。
-
架構:強調設計複用;
定義程式的體系結構,開發人員通過預留的接口插入代碼(做填空題)——把自己的錘子裝在流水線上,讓它砸核桃。
-
設計模式:複用解決方案;
設計模式提供了解決一類問題的有效經驗,複用這些經驗往往可以很好地解決問題——看别人是怎麼砸核桃的,依葫蘆畫瓢模仿一遍。
控制反轉執行個體
我們日常工作中,相信以下代碼大家非常熟悉了:
// 架構中的工具類
public class xxxxUtils {
public static boolean doSomething() {
// ... 架構中的固定方法
}
}
// 需要自定義邏輯
public class UserServiceTest {
public static void main(String[] args) {
if (doSomething()) {
System.out.println("Test succeed.");
} else {
System.out.println("Test failed.");
}
}
}
在上面的代碼中,所有的流程都由程式員來控制。隻有核心的代碼可能會需要調用封裝好的方法來執行。
我們再看看控制反轉下,如何實作該執行個體:
// 架構中的類
public abstract class xxxxHandler {
public void run() {
if (doSomething()) {
System.out.println("Test succeed.");
} else {
System.out.println("Test failed.");
}
}
public abstract boolean doSomething();
}
// 自己定義的實作
public class MyHandler extends xxxxHandler {
@Overried
public boolean doSomething() {
// ...我自己的業務邏輯
};
}
// 架構中的初始化
public class Application {
private static final List<xxxxHandler> handlers= new ArrayList<>();
public static void register(xxxxHandler handler) {
handlers.add(handler);
}
// 啟動類或者配置類
public static final void main(String[] args) {
for (xxxxHandler handler: handlers) {
handler.doSomething();
}
}
現在,我們隻需要在架構預留的擴充點中擴充,繼承架構中的父類,然後實作其中需要自定義的業務實作,然後注冊到架構中即可,完全不需要關心架構是如何處理的:
// 注冊操作還可以通過配置的方式來實作,不需要程式員顯示調用register()
Application.register(new MyHandler();
架構提供了一個可擴充的代碼骨架,用來組裝對象、管理整個執行流程。程式員利用架構進行開發的時候,隻需要往預留的擴充點上,添加跟自己業務相關的代碼,就可以利用架構來驅動整個程式流程的執行。
這裡的“控制”指的是對程式執行流程的控制,而“反轉”指的是在沒有使用架構之前,程式員自己控制整個程式的執行。在使用架構之後,整個程式的執行流程可以通過架構來控制。流程的控制權從程式員“反轉”到了架構。
實際上,實作控制反轉的方法有很多,除了剛才例子中所示的類似于模闆設計模式的方法之外,還有馬上要講到的依賴注入等方法,是以,控制反轉并不是一種具體的實作技巧,而是一個比較籠統的設計思想,一般用來指導架構層面的設計。
依賴注入
依賴注入其實很簡單:不通過 new() 的方式在類内部建立依賴類對象,而是将依賴的類對象在外部建立好之後,通過構造函數、函數參數等方式傳遞(或注入)給類使用。
比如說Spring的Bean容器就是提前将所有的類對象建立好,在需要的時候直接注入使用。
依賴注入執行個體
// 非依賴注入實作方式
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);
依賴注入執行個體2
我們代碼中通過 Kafka 來發送異步消息。對于這樣一個功能的開發,我們要學會将其抽象成一組跟具體消息隊列(Kafka)無關的異步消息接口。所有上層系統都依賴這組抽象的接口程式設計,并且通過依賴注入的方式來調用。當我們要替換新的消息隊列的時候,比如将 Kafka 替換成 RocketMQ,可以很友善地拔掉老的消息隊列實作,插入新的消息隊列實作。具體代碼如下所示:
// 這一部分展現了抽象意識
public interface MessageQueue { //... }
public class KafkaMessageQueue implements MessageQueue { //... }
public class RocketMQMessageQueue implements MessageQueue {//...}
public interface MessageFromatter { //... }
public class JsonMessageFromatter implements MessageFromatter {//...}
public class ProtoBufMessageFromatter implements MessageFromatter {//...}
public class Demo {
private MessageQueue msgQueue; // 基于接口而非實作程式設計
public Demo(MessageQueue msgQueue) { // 依賴注入
this.msgQueue = msgQueue;
}
// msgFormatter:多态、依賴注入
public void sendNotification(Notification notification, MessageFormatter msgFormatter) {
//...
}
}