設計模式之美 - 19 | 理論五:控制反轉、依賴反轉、依賴注入,這三者有何區別和聯繫?

這系列相關博客,參考 設計模式之美

設計模式之美 - 19 | 理論五:控制反轉、依賴反轉、依賴注入,這三者有何區別和聯繫?

關於 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 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 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 和應用程序代碼之間並沒有直接的依賴關係,兩者都依賴同一個“抽象”,也就是 Sevlet 規範。Servlet規範不依賴具體的 Tomcat 容器和應用程序的實現細節,而 Tomcat 容器和應用程序依賴 Servlet 規範。

重點回顧

好了,今天的內容到此就講完了。我們一塊來總結回顧一下,你需要掌握的重點內容。

1. 控制反轉
實際上,控制反轉是一個比較籠統的設計思想,並不是一種具體的實現方法,一般用來指導框架層面的設計。這裏所說的“控制”指的是對程序執行流程的控制,而“反轉”指的是在沒有使用框架之前,程序員自己控制整個程序的執行。在使用框架之後,整個程序的執行流程通過框架來控制。流程的控制權從程序員“反轉”給了框架。

2. 依賴注入
依賴注入和控制反轉恰恰相反,它是一種具體的編碼技巧。我們不通過 new 的方式在類內部創建依賴類的對象,而是將依賴的類對象在外部創建好之後,通過構造函數、函數參數等方式傳遞(或注入)給類來使用。

3. 依賴注入框架
我們通過依賴注入框架提供的擴展點,簡單配置一下所有需要的類及其類與類之間依賴關係,就可以實現由框架來自動創建對象、管理對象的生命週期、依賴注入等原本需要程序員來做的事情。

4. 依賴反轉原則
依賴反轉原則也叫作依賴倒置原則。這條原則跟控制反轉有點類似,主要用來指導框架層面的設計。高層模塊不依賴低層模塊,它們共同依賴同一個抽象。抽象不要依賴具體實現細節,具體實現細節依賴抽象。

課堂討論

從 Notification 這個例子來看,“基於接口而非實現編程”跟“依賴注入”,看起來非常類似,那它倆有什麼區別和聯繫呢?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章