很長一段時間裡,我對控制反轉和依賴注入這兩個概念很模糊,閉上眼睛想一想,總有一種眩暈的感覺。但為了成為一名優秀的 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 隻管使用就可以。