非Java程序員竟鮮有人真正理解DI和IOC

前言

小編在後端圈也算是閱人無數了, 發現一個現象,Java程序員對於面嚮對象語言的基礎知識整體掌握比較紮實,而類似PHP,Python的初級甚至中級程序員就比較薄弱,比如說DI和IOC,很少有PHP程序員能理解的很準確。

這裏, 我希望通過最簡單的話語與大家分享. 依賴注入和控制反轉兩個概念讓很多初學這迷惑, 覺得玄之又玄,高深莫測. 這裏想先說明兩點:

  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裏是否有很多共用的邏輯代碼,如果是,就選擇父類, 否則就選接口. 這裏我們選擇接口的辦法: 新增Coder接口, 文件: Coder.java

package demo;
public interface Coder {
    public void writeCode();
}

修改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");
    }
}

新類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");
    }
}

修改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();
    }
}

修改用於測試的類使用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)開發,使用框架時,框架控制着整個運行過程.

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

簡單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();
     }
}

簡單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." )這是好萊塢電影公司對面試者常見的答覆.事實上,不只電影行業,基本上所有公司人力資源部對面試者都這樣說. 讓面試者從主動聯繫轉換爲被動等待.

爲了增加本文的趣味性,這裏在舉個比喻講述控制反轉.人們談戀愛,在以前通常是男追女,現在時代進步了,女追男也很常見.這也是控制反轉。體會下你追女孩和女孩追你的區別:

  • 你追女孩時,你是主動的,你是標準制定者, 要求身高多少,顏值多少,滿足你的標準,你纔去追,追誰,什麼時候追, 你說了算. 這就類似,框架制定接口規範,對實現了接口的類調用.
  • 等女孩追你時,你是被動的,她是標準制定者,要求有車,有房等,你買車,買房,努力工作掙錢,是爲了達到標準(既實現接口規範), 你萬事具備, 處於候追狀態, 但時誰來追你,什麼時候追,你不知道.

這就是主動和被動的區別,也是爲什麼男的偏好主動的原因. 這裏模仿好萊塢原則,提一箇中國帥哥原則:"不要追哥, 哥來追你(如果合適)"。

第三節 總結

  • 控制反轉是一種在軟件工程中解耦合的思想,調用類只依賴接口,而不依賴具體的實現類,減少了耦合。控制權交給了容器,在運行的時候才由容器決定將具體的實現動態的“注入”到調用類的對象中。
  • 依賴注入是一種設計模式,可以作爲控制反轉的一種實現方式。依賴注入就是將實例變量傳入到一個對象中去(Dependency injection means giving an object its instance variables)。
  • 通過IoC框架,類A依賴類B的強耦合關係可以在運行時通過容器建立,也就是說把創建B實例的工作移交給容器,類A只管使用就可以。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章