輕松理解 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接口,
    文件: Coder.java
package demo; public interface Coder { public void writeCode(); } 
  1. 修改Phper類實現Coder接口
    文件: 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接口
    文件: 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的依賴.
    文件: 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交出賦值權, 從主動賦值到被動賦值, 這就是控制反轉.

第二節 控制反轉 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. 面向對象的設計思想
    第一節 提到在面向對象設計中,名詞皆對象,這里做些補充.
    當面對一個項目,做系統設計時,第一個問題就是,系統里要設計哪些類?
    最簡單的辦法就是,把要設計系統的名詞提出來,通常,名詞可設計為對象,
    但是否所有名詞都需要設計對應的類呢? 要具體問題具體分析.不是不可以,是否有必要.
    有時候需要把一些動詞名詞化, 看看現實生活中, 寫作是動詞,所有寫作的人叫什么? 沒有合適的稱呼,我們就叫作者, 閱讀是動詞,閱讀的人就稱讀者. 中文通過加"者","手"使動詞名詞化,舞者,歌手,投手,射手皆是這類.
    英語世界也類似,通過er, or等后綴使動詞名詞化, 如singer,writer,reader,actor, visitor.
    現實生活這樣, Java世界也一樣.
    Java通過able,or后綴使動詞名詞化.如Runnable,Serializable,Parcelable Comparator,Iterator.
    Runnable即可以運行的東西(類) ,其他類似.
    了解了動詞名詞化,對java里的很多類就容易理解了.

  2. 相關術語(行話)解釋
    Java 里術語滿天飛, 讓初學者望而生畏. 如果你不想讓很多術語影響學習,這一節可忽視.
    了解了原理,叫什么並不重要. 了解些術語的好處是便於溝通和閱讀外文資料,還有就是讓人看起來很專業的樣子.

  • 耦合(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 的設計就是.

希望上述內容, 對大家有所幫助, 謝謝.

進一段廣告

快才助手, 在電腦上操作手機, Android屏幕同步軟件
本文作者手工打造,熱情推薦,網址: http://www.kwaicai.com



作者:黃洪清
鏈接:https://www.jianshu.com/p/506dcd94d4f9
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

 


免責聲明!

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



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