03.23 輕鬆理解 Java開發中的依賴注入(DI)和控制反轉(IOC)

輕鬆理解 Java開發中的依賴注入(DI)和控制反轉(IOC)

前言

關於這個話題, 網上有很多文章,這裡, 我希望通過最簡單的話語與大家分享.

依賴注入和控制反轉兩個概念讓很多初學這迷惑, 覺得玄之又玄,高深莫測.

這裡想先說明兩點:

  1. 依賴注入和控制反轉不是高級的,很初級,也很簡單.
  2. 在JAVA世界,這兩個概念像空氣一樣無所不在,徹底理解很有必要.

第一節 依賴注入 Dependency injection

這裡通過一個簡單的案例來說明.

在公司裡有一個常見的案例: "把任務指派個程序員完成".

把這個案例用面向對象(OO)的方式來設計,通常在面向對象設計中,名詞皆可設計為對象

這句話裡"任務","程序員"是名詞,所以我們考慮創建兩個Class: Task 和 Phper (php 程序員)

Step1 設計

文件: Phper.java

package demo;
public class Phper {
private String name;
public Phper(String name){
this.name=name;
}
public void writeCode(){
System.out.println(this.name + " is writing php code");
}
}

文件: Task.java

package demo;
public class Task {
private String name;
private Phper owner;
public Task(String name){
this.name =name;
this.owner = new Phper("zhang3");
}
public void start(){
System.out.println(this.name+ " started");
this.owner.writeCode();
}
}

文件: MyFramework.java, 這是個簡單的測試程序.

package demo;
public class MyFramework {
public static void main(String[] args) {
Task t = new Task("Task #1");
t.start();
}
}

運行結果:

Task #1 started

hang3 is writing php code

我們看一看這個設計有什麼問題?

如果只是為了完成某個臨時的任務,程序即寫即仍,這沒有問題,只要完成任務即可.

但是如果同事仰慕你的設計,要重用你的代碼.你把程序打成一個類庫(jar包)發給同事.

現在問題來了,同事發現這個Task 類 和 程序員 zhang3 綁定在一起,他所有創建的Task,都是程序員zhang3負責,他要把一些任務指派給Lee4, 就需要修改Task的源程序, 如果沒有Task的源程序,就無法把任務指派給他人. 而通常類庫(jar包)的使用者通常不需要也不應該來修改類庫的源碼,如果大家都來修改類庫的源碼,類庫就失去了重用的設計初衷.

我們很自然的想到,應該讓用戶來指派任務負責人. 於是有了新的設計.

Step2 設計:

文件: Phper.java 不變.

文件: Task.java

package demo;
public class Task {
private String name;
private Phper owner;
public Task(String name){
this.name =name;
}
public void setOwner(Phper owner){
this.owner = owner;
}

public void start(){
System.out.println(this.name+ " started");
this.owner.writeCode();
}
}

文件: MyFramework.java, 這是個簡單的測試程序.

package demo;
public class MyFramework {
public static void main(String[] args) {
Task t = new Task("Task #1");
Phper owner = new Phper("lee4");
t.setOwner(owner);
t.start();
}
}

這樣用戶就可在使用時指派特定的PHP程序員.

我們知道,任務依賴程序員,Task類依賴Phper類,之前,Task類綁定特定的實例,現在這種依賴可以在使用時按需綁定,這就是依賴注入(DI).

這個例子,我們通過方法setOwner注入依賴對象,

另外一個常見的注入辦法是在Task的構造函數注入:

 public Task(String name,Phper owner){
this.name = name;
this.owner = owner;
}

在Java開發中,把一個對象實例傳給一個新建對象的情況十分普遍,通常這就是注入依賴.

Step2 的設計實現了依賴注入.

我們來看看Step2 的設計有什麼問題.

如果公司是一個單純使用PHP的公司,所有開發任務都有Phper 來完成,這樣這個設就已經很好了,不用優化.

但是隨著公司的發展,有些任務需要JAVA來完成,公司招了寫Javaer (java程序員),現在問題來了,這個Task類庫的的使用者發現,任務只能指派給Phper,

一個很自然的需求就是Task應該即可指派給Phper也可指派給Javaer.

Step3 設計

我們發現不管Phper 還是 Javaer 都是Coder(程序員), 把Task類對Phper類的依賴改為對Coder 的依賴即可.

這個Coder可以設計為父類或接口,Phper 或 Javaer 通過繼承父類或實現接口 達到歸為一類的目的.

選擇父類還是接口,主要看Coder裡是否有很多共用的邏輯代碼,如果是,就選擇父類

否則就選接口.

這裡我們選擇接口的辦法:

  1. 新增Coder接口,
  2. 文件: Coder.java
package demo;
public interface Coder {
public void writeCode();
}
  1. 修改Phper類實現Coder接口
  2. 文件: Phper.php
package demo;
public class Phper implements Coder {
private String name;
public Phper(String name){
this.name=name;
}
public void writeCode(){
System.out.println(this.name + " is writing php code");
}
}
  1. 新類Javaer實現Coder接口
  2. 文件: Javaer.php
package demo;
public class Javaer implements Coder {
private String name;
public Javaer(String name){
this.name=name;
}
public void writeCode(){
System.out.println(this.name + " is writing java code");
}
}
  1. 修改Task由對Phper類的依賴改為對Coder的依賴.
  2. 文件: Task.java
package demo;
public class Task {
private String name;
private Coder owner;
public Task(String name){
this.name =name;
}
public void setOwner(Coder owner){
this.owner = owner;
}
public void start(){
System.out.println(this.name+ " started");
this.owner.writeCode();
}
}
  1. 修改用於測試的類使用Coder接口:
package demo;
public class MyFramework {
public static void main(String[] args) {
Task t = new Task("Task #1");
// Phper, Javaer 都是Coder,可以賦值
Coder owner = new Phper("lee4");
//Coder owner = new Javaer("Wang5");
t.setOwner(owner);
t.start();
}
}

現在用戶可以和方便的把任務指派給Javaer 了,如果有新的Pythoner加入,沒問題.

類庫的使用者只需讓Pythoner實現(implements)了Coder接口,就可把任務指派給Pythoner, 無需修改Task 源碼, 提高了類庫的可擴展性.

回顧一下,我們開發的Task類,

在Step1 中與Task與特定實例綁定(zhang3 Phper)

在Step2 中與Task與特定類型綁定(Phper)

在Step3 中與Task與特定接口綁定(Coder)

雖然都是綁定, 從Step1,Step2 到 Step3 靈活性可擴展性是依次提高的.

Step1 作為反面教材不可取, 至於是否需要從Step2 提升為Step3, 要看具體情況.

如果依賴的類型是唯一的Step2 就可以, 如果選項很多就選Step3設計.

依賴注入(DI)實現了控制反轉(IoC)的思想.

看看怎麼反轉的?

Step1 程序

this.owner = new Phper("zhang3");

Step1 設計中 任務Task 依賴負責人owner, 就主動新建一個Phper 賦值給owner,

這裡是新建,也可能是在容器中獲取一個現成的Phper,新建還是獲取,無關緊要,關鍵是賦值, 主動賦值. 這裡提一個賦值權的概念.

在Step2 和 Step3, Task 的 owner 是被動賦值的.誰來賦值,Task自己不關心,可能是類庫的用戶,也可能是框架或容器.

Task交出賦值權, 從主動賦值到被動賦值, 這就是控制反轉.

輕鬆理解 Java開發中的依賴注入(DI)和控制反轉(IOC)

第二節 控制反轉 Inversion of control

什麼是控制反轉 ?

簡單的說從主動變被動就是控制反轉.

上文以依賴注入的例子,對控制反轉做了個簡單的解釋.

控制反轉是一個很廣泛的概念, 依賴注入是控制反轉的一個例子,但控制反轉的例子還很多,甚至與軟件開發無關.

這有點類似二八定律,人們總是用具體的實例解釋二八定律,具體的實例不等與二八定律(不瞭解二八定律的朋友,請輕鬆忽略這個類比)

現在從其他方面談一談控制反轉.

傳統的程序開發,人們總是從main 函數開始,調用各種各樣的庫來完成一個程序.

這樣的開發,開發者控制著整個運行過程.

而現在人們使用框架(Framework)開發,使用框架時,框架控制著整個運行過程.

對比以下的兩個簡單程序:

  1. 簡單java程序
package demo;
public class Activity {
public Activity(){
this.onCreate();
}
public void onCreate(){
System.out.println("onCreate called");
}
public void sayHi(){
System.out.println("Hello world!");
}
public static void main(String[] args) {
Activity a = new Activity();
a.sayHi();
}
}
  1. 簡單Android程序
package demo;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends Activity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
TextView tv = new TextView(this);
tv.append("Hello ");
tv.append("world!");
setContentView(tv);
}
}

這兩個程序最大的區別就是,前者程序的運行完全由開發控制,後者程序的運行由Android框架控制.

兩個程序都有個onCreate方法.

前者程序中,如果開發者覺得onCreate 名稱不合適,想改為Init,沒問題,直接就可以改, 相比下,後者的onCreate 名稱就不能修改.

因為,後者使用了框架,享受框架帶來福利的同時,就要遵循框架的規則.

這就是控制反轉.

可以說, 控制反轉是所有框架最基本的特徵.

也是框架和普通類庫最大的不同點.

很多Android開發工程師在享用控制反轉帶來的便利,去不知什麼是控制反轉.

就有點像深海里的魚不知到什麼是海水一樣.

通過框架可以把許多共用的邏輯放到框架裡,讓用戶專注自己程序的邏輯.

這也是為什麼現在,無論手機開發,網頁開發,還是桌面程序, 也不管是Java,PHP,還是Python框架無處不在.

回顧下之前的文件: MyFramework.java

package demo;
public class MyFramework {
public static void main(String[] args) {
Task t = new Task("Task #1");

Coder owner = new Phper("lee4");
t.setOwner(owner);
t.start();
}
}

這只是簡單的測試程序,取名為MyFramework, 是因為它擁有框架3個最基本特徵

  1. main函數,即程序入口.
  2. 創建對象.
  3. 裝配對象.(setOwner)

這裡創建了兩個對象,實際框架可能會創建數千個對象,可能通過工廠類而不是直接創建,

這裡直接裝配對象,實際框架可能用XML 文件描述要創建的對象和裝配邏輯.

當然實際的框架還有很多這裡沒涉及的內容,只是希望通過這個簡單的例子,大家對框架有個初步認識.

控制反轉還有一個漂亮的比喻:

好萊塢原則(Hollywood principle)

"不要打電話給我們,我們會打給你(如果合適)" ("don't call us, we'll call you." )

這是好萊塢電影公司對面試者常見的答覆.

事實上,不只電影行業,基本上所有公司人力資源部對面試者都這樣說.

讓面試者從主動聯繫轉換為被動等待.

為了增加本文的趣味性,這裡在舉個比喻講述控制反轉.

人們談戀愛,在以前通常是男追女,現在時代進步了,女追男也很常見.

這也是控制反轉

體會下你追女孩和女孩追你的區別:

你追女孩時,你是主動的,你是標準制定者, 要求身高多少,顏值多少,滿足你的標準,你才去追,追誰,什麼時候追, 你說了算.

這就類似,框架制定接口規範,對實現了接口的類調用.

等女孩追你時,你是被動的,她是標準制定者,要求有車,有房等,你買車,買房,努力工作掙錢,是為了達到標準(既實現接口規範), 你萬事具備, 處於候追狀態, 但時誰來追你,什麼時候追,你不知道.

這就是主動和被動的區別,也是為什麼男的偏好主動的原因.

這裡模仿好萊塢原則,提一箇中國帥哥原則:"不要追哥, 哥來追你(如果合適)",

簡稱CGP.( Chinese gentleman principle: "don't court me, I will court you")

擴展話題

  1. 面向對象的設計思想
  2. 第一節 提到在面向對象設計中,名詞皆對象,這裡做些補充.
  3. 當面對一個項目,做系統設計時,第一個問題就是,系統裡要設計哪些類?
  4. 最簡單的辦法就是,把要設計系統的名詞提出來,通常,名詞可設計為對象,
  5. 但是否所有名詞都需要設計對應的類呢? 要具體問題具體分析.不是不可以,是否有必要.
  6. 有時候需要把一些動詞名詞化, 看看現實生活中, 寫作是動詞,所有寫作的人叫什麼? 沒有合適的稱呼,我們就叫作者, 閱讀是動詞,閱讀的人就稱讀者. 中文通過加"者","手"使動詞名詞化,舞者,歌手,投手,射手皆是這類.
  7. 英語世界也類似,通過er, or等後綴使動詞名詞化, 如singer,writer,reader,actor, visitor.
  8. 現實生活這樣, Java世界也一樣.
  9. Java通過able,or後綴使動詞名詞化.如Runnable,Serializable,Parcelable Comparator,Iterator.
  10. Runnable即可以運行的東西(類) ,其他類似.
  11. 瞭解了動詞名詞化,對java裡的很多類就容易理解了.
  12. 相關術語(行話)解釋
  13. Java 裡術語滿天飛, 讓初學者望而生畏. 如果你不想讓很多術語影響學習,這一節可忽視.
  14. 瞭解了原理,叫什麼並不重要. 瞭解些術語的好處是便於溝通和閱讀外文資料,還有就是讓人看起來很專業的樣子.
  • 耦合(couple): 相互綁定就是耦合第一節 Step1,Step2,Step3 都是.
  • 緊耦合(Tight coupling) Step1 中,Task 和 zhang3 綁在一起; Step2中 Task 和 Phper 綁在一起, 都是.
  • 松耦合(Loose coupling) Step3 中,Task 和 Coder 接口綁在一起就是
  • 解耦(Decoupling): 從Step1 , Step2, 到 Step3 的設計就是Decoupling, 讓對象可以靈活組合.
  • 上溯造型或稱向上轉型(Upcasting). 把一個對像賦值給自己的接口或父類變量就是.因為畫類圖時接口或父類在畫在上面,所以是Upcasting. Step3中一下程序就是:

Coder owner = new Phper("lee4");

  • 下溯造型或稱向下轉型(Downcasting). 和Upcasting 相反,把Upcasting過後的對象轉型為之前的對象. 這個上述程序不涉及,順帶說一下

Coder owner = new Phper("lee4");

Phper p = (Phper) owner;

  • 注入(Inject): 通過方法或構造函數把一個對象傳遞給另一個對象. Step3 中的setOwner 就是.
  • 裝配(Assemble): 和上述注入是一個意思,看個人喜好使用.
  • 工廠(Factory): 如果一個類或對象專門負責創建(new) 對象,這個類或對象就是工廠
  • 容器(Container): 專門負責存放創建好的對象的東西. 可以是個Hash表或 數組.
  • 面向接口編程(Interface based programming) Step3 的設計就是.
輕鬆理解 Java開發中的依賴注入(DI)和控制反轉(IOC)


分享到:


相關文章: