天天看點

設計原則之【依賴反轉原則】依賴反轉、控制反轉、依賴注入,都是什麼意思?

文章目錄

  • ​​依賴反轉原則​​
  • ​​控制反轉​​
  • ​​怎麼了解“反轉”?​​
  • ​​控制反轉的好處​​
  • ​​控制反轉執行個體​​
  • ​​依賴注入​​
  • ​​依賴注入執行個體​​
  • ​​依賴注入執行個體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) {
    //...    
  }
}      

參考資料