Java:控制反轉(IoC)與依賴注入(DI)

沉默王二發表於2019-08-07

很長一段時間裡,我對控制反轉和依賴注入這兩個概念很模糊,閉上眼睛想一想,總有一種眩暈的感覺。但為了成為一名優秀的 Java 工程師,我花了一週的時間,徹底把它們搞清楚了。

01、緊耦合

在我們編碼的過程中,通常都需要兩個或者更多的類通過彼此的合作來實現業務邏輯,也就是說,某個物件需要獲取與其合作物件的引用,如果這個獲取的過程需要自己實現,程式碼的耦合度就會高,維護起來的成本就比較高。

我們來通過實戰模擬一下。假如老王是少林寺的主持,他想讓小二和尚去掃達摩院的地,程式碼可以這樣實現。

小二類的程式碼如下所示:

public class Xiaoer {
    public void saodi() {
        System.out.println("小二我在掃達摩院的地");
    }
}

老王類的程式碼如下所示:

public class Laowang {
    public void mingling() {
        new Xiaoer().saodi();
    }
}

測試類的程式碼如下所示:

public class Test {

    public static void main(String[] args) {
        Laowang laowang = new Laowang();
        laowang.mingling();
    }

}

Laowang 類的 mingling 方法中使用 new 關鍵字建立了一個 Xiaoer 類的物件——這種程式碼的耦合度就很高,維護起來的成本就很高,為什麼這麼說呢?

某一天,達摩院的地又髒了,老王主持想起了小二和尚,可小二和尚去練易筋經了,讓誰去掃地呢,老王主持想起了小三和尚,於是 Laowang 類就不得不重新下一個新的命令,於是程式碼變成了這樣:

public class Xiaosan {
    public void saodi() {
        System.out.println("小三我在掃達摩院的地");
    }
}
public class Laowang {
    public void mingling() {
        new Xiaoer().saodi();
    }

    public void mingling1() {
        new Xiaosan().saodi();
    }
}

假如小三和尚去挑水了,老王主持沒準要下命令給小四和尚去掃達摩院的地。這樣下去的話,Laowang 這個類會瘋掉的。

老王主持覺得自己堂堂一屆高僧,下個掃地的命令竟然這樣麻煩,他覺得很不爽。

02、控制反轉

我們得替老王主持想個辦法對不對?

不如把這個掃地的差事交給老王的師弟老方吧,老方負責去叫小二和尚還是小三和尚還是小四和尚去執行老王主持的命令。程式碼可以這樣實現。

定義一個掃地和尚的介面,程式碼如下所示:

public interface Heshang {
    void saodi();
}

小二類的程式碼修改如下所示:

public class Xiaoer implements Heshang {

    @Override
    public void saodi() {
        System.out.println("小二我在掃達摩院的地");        
    }

    public boolean isYijinjing() {
        // 星期三的時候小二和尚要練易筋經
        return false;
    }
}

小三類的程式碼修改如下所示:

public class Xiaosan implements Heshang {

    @Override
    public void saodi() {
        System.out.println("小三我在掃達摩院的地");        
    }
}

老方類的程式碼如下所示:

public class Laofang {
    public static Heshang getSaodiseng() {
        Xiaoer xiaoer = new Xiaoer();
        if (xiaoer.isYijinjing()) {
            return new Xiaosan();
        }
        return xiaoer;
    }
}

如果老方確認小二和尚在練易筋經,就叫小三和尚。

老王類的程式碼修改如下所示:

public class Laowang {
    public void mingling() {
        Laofang.getSaodiseng().saodi();
    }
}

測試類的程式碼不改變,如下所示:

public class Test {

    public static void main(String[] args) {
        Laowang laowang = new Laowang();
        laowang.mingling();
    }

}

老王現在是不是省心多了,他只管下命令,該叫誰去掃達摩院的地由他師弟老方去負責。

我們替老王想的這個辦法就叫控制反轉(Inversion of Control,縮寫為 IoC),它不是一種技術,而是一種思想——指導我們設計出鬆耦合的程式。

控制反轉從詞義上可以拆分為“控制”和“反轉”,說到控制,就必須找出主語和賓語,誰控制了誰;說到反轉,就必須知道正轉是什麼。

你看,在緊耦合的情況下,老王下命令的時候自己要通過 new 關鍵字建立依賴的物件(小二和尚或者小三和尚);而控制反轉後,老王要找的掃地和尚由他師弟老方負責,也就是說控制權交給了老方,是不是反轉了呢?

03、依賴注入

依賴注入(Dependency Injection,簡稱 DI)是實現控制反轉的主要方式:在類 A 的例項建立過程中就建立了依賴的 B 物件,通過型別或名稱來判斷將不同的物件注入到不同的屬性中。大概有 3 種具體的實現形式:

1)基於建構函式。實現特定引數的建構函式,在新建物件時傳入所依賴型別的物件。

老王類的程式碼修改如下所示:

public class Laowang {
    private Heshang saodiseng;

    public Laowang(Heshang saodiseng) {
        this.saodiseng = saodiseng;
    }
    public void mingling() {
       this.saodiseng.saodi();
    }
}

測試類的程式碼修改如下所示:

public class Test {

    public static void main(String[] args) {
        Laowang laowang = new Laowang(new Xiaosan());
        laowang.mingling();
    }

}

這時候,控制權掌握在測試類的手裡,它決定派小二和尚還是小三和尚去執行老王的掃地命令。

2)基於 set 方法。實現特定屬性的 public set 方法,讓外部容器呼叫傳入所依賴型別的物件。

老王類的程式碼修改如下所示:

public class Laowang {
    private Heshang saodiseng;

    public Heshang getSaodiseng() {
        return saodiseng;
    }

    public void setSaodiseng(Heshang saodiseng) {
        this.saodiseng = saodiseng;
    }

    public void mingling() {
       this.getSaodiseng().saodi();
    }
}

測試類的程式碼修改如下所示:

public class Test {

    public static void main(String[] args) {
        Laowang laowang = new Laowang();
        Xiaosan xiaosan = new Xiaosan();
        laowang.setSaodiseng(xiaosan);
        laowang.mingling();
    }

}

這時候,控制權仍然掌握在測試類的手裡,它決定派小二和尚還是小三和尚去執行老王的掃地命令。

3)基於介面。實現特定介面以供外部容器注入所依賴型別的物件,這種做法比較建構函式和 set 方法更為複雜,這裡就此略過。

可能有人會把控制反轉等同於依賴注入,但實際上它們有著本質上的不同:控制反轉是一種思想,而依賴注入是實現控制反轉的一種形式。

04、Spring 框架

當我們搞清楚控制反轉和依賴注入的概念後,就可以順帶了解一下大名鼎鼎的 Spring 框架。控制反轉是 Spring 框架的核心,貫穿始終。Spring 中依賴注入有兩種實現方式:set 方式(傳值方式)和構造器方式(引用方式)。

首先,我們需要在 pom.xml 檔案中加入 Spring 的依賴項,程式碼如下所示:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>4.3.2.RELEASE</version>
</dependency>

其次,我們將 Laowang 類修改為如下內容:

public class Laowang {
    private Heshang saodiseng;

    public Laowang(Heshang saodiseng) {
        this.saodiseng = saodiseng;
    }
    public void mingling() {
       this.saodiseng.saodi();
    }
}

然後,我們建立一個 Spring 的配置檔案 application.xml,內容如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="laowang" class="com.cmower.java_demo.ioc.Laowang">
    <constructor-arg ref="saodiseng" />
  </bean>

  <bean id="saodiseng" class="com.cmower.java_demo.ioc.Xiaosan" />

</beans>

通過 元素配置了兩個物件,一個老王主持,一個小三和尚,使用 元素將小三和尚作為老王主持的構造引數。

準備工作完成以後,我們來測試一下,程式碼示例如下:

public class Test {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
        Laowang laowang = (Laowang) context.getBean("laowang");
        laowang.mingling();
    }

}

你看,我們將控制權交給了 IoC 框架 Spring,這樣也可以完美的解決程式碼耦合度較緊的問題。

05、最後

總結一下:

1)控制反轉是一種在軟體工程中解耦合的思想,把控制權交給了第三方,在執行的時候由第三方決定將具體的依賴物件“注入”到呼叫類的物件中。

2)依賴注入可以作為控制反轉的一種實現方式,將例項變數傳入到一個物件中去。

3)通過 IoC 框架,類 A 依賴類 B 的強耦合關係可以在執行時通過容器建立,也就是說把建立 B 例項的工作移交給容器,類 A 只管使用就可以。

 

相關文章