理論五:控制反轉、依賴反轉、依賴注入,這三者有何區別和聯系?


關於 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 規范。

  1. 控制反轉
    實際上,控制反轉是一個比較籠統的設計思想,並不是一種具體的實現方法,一般用來指導框架層面的設計。這里所說的“控制”指的是對程序執行流程的控制,而“反轉”指的是在沒有使用框架之前,程序員自己控制整個程序的執行。在使用框架之后,整個程序的執行流程通過框架來控制。流程的控制權從程序員“反轉”給了框架。
  2. 依賴注入
    依賴注入和控制反轉恰恰相反,它是一種具體的編碼技巧。我們不通過 new 的方式在類內部創建依賴類的對象,而是將依賴的類對象在外部創建好之后,通過構造函數、函數參數等方式傳遞(或注入)給類來使用。
  3. 依賴注入框架
    我們通過依賴注入框架提供的擴展點,簡單配置一下所有需要的類及其類與類之間依賴關系,就可以實現由框架來自動創建對象、管理對象的生命周期、依賴注入等原本需要程序員來做的事情。
  4. 依賴反轉原則
    依賴反轉原則也叫作依賴倒置原則。這條原則跟控制反轉有點類似,主要用來指導框架層面的設計。高層模塊不依賴低層模塊,它們共同依賴同一個抽象。抽象不要依賴具體實現細節,具體實現細節依賴抽象。

“基於接口而非實現編程”與“依賴注入”的聯系是二者都是從外部傳入依賴對象而不是在內部去new一個出來。
區別是“基於接口而非實現編程”強調的是“接口”,強調依賴的對象是接口,而不是具體的實現類;而“依賴注入”不強調這個,類或接口都可以,只要是從外部傳入不是在內部new出來都可以稱為依賴注入。

區別:
1.依賴注入是一種具體編程技巧,關注的是對象創建和類之間關系,目的提高了代碼的擴展性,我們可以靈活地替換依賴的類。
2.基於接口而非實現編程是一種設計原則,關注抽象和實現,上下游調用穩定性,目的是降低耦合性,提高擴展性。

聯系:
都是基於開閉原則思路,提高代碼擴展性!

依賴倒置原則概念是高層次模塊不依賴於低層次模塊。看似在要求高層次模塊,實際上是在規范低層次模塊的設計。

低層次模塊提供的接口要足夠的抽象、通用,在設計時需要考慮高層次模塊的使用種類和場景。

明明是高層次模塊要使用低層次模塊,對低層次模塊有依賴性。現在反而低層次模塊需要根據高層次模塊來設計,出現了「倒置」的顯現。

這樣設計好處有兩點:

  1. 低層次模塊更加通用,適用性更廣
  2. 高層次模塊沒有依賴低層次模塊的具體實現,方便低層次模塊的替換

思考題:

基於接口而非實現編程,是一種指導編碼的思想。依賴注入是它的一種具體應用

1⃣️控制反轉是一種編程思想,把控制權交給第三方。依賴注入是實現控制反轉最典型的方法。
2⃣️依賴注入(對象)的方式要采用“基於接口而非實現編程”的原則,說白了就是依賴倒轉。
3⃣️低層的實現要符合里氏替換原則。子類的可替換性,使得父類模塊或依賴於抽象的高層模塊無需修改,實現程序的可擴展性。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM