天天看點

依賴注入 DI 控制反轉 IOC 概念 案例 [MD]

博文位址

我的GitHub 我的部落格 我的微信 我的郵箱
baiqiantao bqt20094 [email protected]

目錄

  • 控制反轉 IOC
    • 什麼是控制反轉
    • 兩個簡單程式
    • 差別
  • 依賴注入 Dependency injection
    • Step1 設計
    • Step2 設計
    • Step3 設計
    • 總結
    • 依賴注入實作了控制反轉的思想
    • JAVA中依賴注入的幾種方式
      • 通過set方法注入
      • 通過構造器注入
      • 通過注解注入
      • 通過接口注入

Inversion Of Control

簡單的說,

從主動變被動就是控制反轉

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

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

開發者控制着整個運作過程

。而現在人們使用架構(Framework)開發,使用架構時,

架構控制着整個運作過程

簡單java程式

public class Activity {
    public Activity() {
        this.onCreate();//------------開發者主動調用onCreate()方法------------
    }
    public void onCreate() {
        System.out.println("onCreate called");
    }
    public static void main(String[] args) {
        Activity a = new Activity();
    }
}
           

簡單Android程式

public class MainActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) { //------------架構自主調用onCreate()方法------------
        super.onCreate(savedInstanceState);
        System.out.println("onCreate called");
    }
}
           

這兩個程式最大的差別就是,

前者程式的運作完全由開發控制,後者程式的運作由Android架構控制

雖然兩個程式都有個onCreate方法,但在前者程式中,如果開發者覺得onCreate名稱不合适,想改為Init,沒問題,直接就可以改; 相比下,後者的onCreate名稱就不能修改,因為後者使用了架構,享受架構帶來福利的同時,就要遵循架構的規則。

這就是控制反轉。

可以說,

控制反轉是所有架構最基本的特征,也是架構和普通類庫最大的不同點

控制反轉還有一個漂亮的比喻,好萊塢原則(Hollywood principle):

don't call us, we'll call you。

不要打電話給我們,我們會打給你(如果合适)

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

事實上,不隻電影行業,基本上所有公司人力資源部對面試者都會說類似的話,讓面試者從主動聯系轉換為被動等待。

這裡通過一個簡單的案例來說明。在公司裡有一個常見的案例:把任務指派個程式員完成。

把這個案例用面向對象的方式來設計,我們建立兩個類:Task 和 Phper (php 程式員)

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

測試:

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

運作結果:

Task #1 started
zhang3is writing php code
           

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

如果同僚仰慕你的設計,要重用你的代碼,你把程式打成一個類庫(jar包)發給同僚。現在問題來了:同僚發現這個Task 類 和 程式員 zhang3 綁定在一起,他所有建立的Task,都是程式員zhang3負責,他要把一些任務指派給Lee4, 就需要修改Task的源程式, 如果沒有Task的源程式,就無法把任務指派給他人。

而類庫(jar包)的使用者通常不需要也不應該來修改類庫的源碼,我們很自然的想到,應該讓使用者來指派任務負責人,于是有了新的設計。

Phper不變。

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();
    }
}
           
public class MyFramework {
     public static void main(String[] args) {
         Task t = new Task("Task #1");
         Phper owner = new Phper("lee4");
         t.setOwner(owner);//使用者在使用時按需指派特定的PHP程式員
         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來完成,公司招了Java程式員,現在問題來了,這個Task類庫的的使用者發現,任務隻能指派給Phper,一個很自然的需求就是,Task應該即可指派給Phper也可指派給Javaer。

我們發現不管Phper 還是 Javaer 都是Coder(程式員), 把Task類對

Phper

類的依賴改為對

Coder

的依賴即可。

新增Coder接口

public interface Coder {
   void writeCode();
}
           

修改Phper類實作Coder接口

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

新類Javaer實作Coder接口

public class Javaer implements Coder {
   private String name;
   public Javaer(String name){
      this.name=name;
   }
    @Override
   public void writeCode(){
      System.out.println(this.name + " is writing Java code");
   }
}
           

修改Task由對Phper類的依賴改為對Coder的依賴

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

測試

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

現在使用者可以和友善的把任務指派給 Javaer 了,如果有新的 Pythoner 加入,沒問題,類庫的使用者隻需讓 Pythoner 實作 Coder 接口,就可把任務指派給 Pythoner, 無需修改 Task 源碼, 提高了類庫的可擴充性。

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

  • 在Step1 中,Task與特定

    執行個體

    綁定(zhang3 Phper)
  • 在Step2 中,Task與特定

    類型

    綁定(Phper)
  • 在Step3 中,Task與特定

    接口綁

    定(Coder)

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

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

依賴注入(DI)實作了控制反轉(IoC)的思想,看看怎麼反轉的:

  • Step1 設計中,任務 Task 依賴負責人 owner, 是以就主動

    建立

    一個 Phper 指派給 owner(這裡可能是建立,也可能是在容器中擷取一個現成的 Phper,是建立還是擷取無關緊要,關鍵是

    主動指派

    )。
  • 在 Step2 和 Step3 中, Task 的 owner 是

    被動指派

    的,誰來指派,Task 自己不關心,可能是類庫的使用者,也可能是架構或容器,Task

    交出指派權

    從主動指派到被動指派

    ,這就是控制反轉。

public class ClassA {
    ClassB classB;
    public void setClassB(ClassB b) {
        classB = b;
    }
}
           

public class ClassA {
    ClassB classB;
    public void ClassA(ClassB b) {
        classB = b;
    }
}
           

public class ClassA {
    @inject ClassB classB;//此時并不會完成注入,還需要依賴注入架構的支援,如RoboGuice,Dagger2
    //...
    public ClassA() {}
}
           

接口

interface InterfaceB {
    void doIt();
}
           

形式一

class A implements InjectB {
    ClassB classB;
    @Override
    public void injectB(ClassB b) {
      classB = b;
    }
}
           

形式二

public class ClassA {
    InterfaceB clzB;
    public void doSomething() {
        clzB = (InterfaceB) Class.forName("...").newInstance();//根據預先在配置檔案中設定的實作類的類名動态加載實作類
        clzB.doIt();
    }
}
           

此種接口注入方式因為具備侵入性,它要求元件必須與特定的接口相關聯,是以實際使用有限。

2019-5-12

本文來自部落格園,作者:白乾濤,轉載請注明原文連結:https://www.cnblogs.com/baiqiantao/p/10852293.html

繼續閱讀