依賴注入

依賴注入

1 概述

1.1 背景

在我們日常生活中,有一個十分形象的例子來解釋耦合,那就是時鐘。當我們拆開時鐘的後蓋可以看見三個大小不一的齒輪,它們分別帶動着時針、分針和秒針的旋轉。但時鐘的傳動裝置只有一個,所以這些齒輪之間相互嚙合,一個帶動另一個旋轉,以此來共同完成一個任務。對於這樣一組齒輪傳動裝置,如果其中一個齒輪出了問題,那麼整個裝置的運行都會受到影響。齒輪組中齒輪之間的嚙合關係,與軟件系統中對象之間的耦合關係非常相似,所以如果在軟件工程中採用這樣的方式來運行的話,缺點是十分明顯的。

在軟件工程中,對象之間的耦合關係是無法避免的,因為只有不同組件之間協同工作才能更好的實現一些功能。但隨着應用規模越來越大,這種對象之間的依賴關係也會隨之越來越複雜,這就會使得整個軟件的內部構造變得亂如細麻,一個組件的問題會傳遞至其他組件,問題的規模也會隨之變得,想處理這些問題也變得十分的棘手。

對象之間耦合度過高的系統,必然會出現牽一髮而動全身的情形。

耦合關係不僅會出現在對象與對象之間,也會出現在軟件系統的各模塊之間,以及軟件系統和硬件系統之間。如何降低系統之間、模塊之間和對象之間的耦合度,是軟件工程永遠追求的目標之一。為了解決對象之間的耦合度過高的問題,軟件專家Michael Mattson提出了IoC理論,用來實現對象之間的「解耦」,目前這個理論已經被成功地應用到實踐當中,很多的J2EE項目均採用了IoC框架產品spring。

image-20201029182105113

圖1

1.2 耦合

1.2.1 緊耦合

通常情況下如果我們需要在一個對象A中使用另一個對象B,我們需要在A代碼編寫時就完整的引用、創建相應的對象B,因此對象A就得負責對象B的整個生命周期。並且對象A的部分代碼會依賴於創建的對象B,以此類推,當對象逐漸增多且都互相依賴時,儘管每個代碼層級負責不同的任務,但是每個層級還是幹了一些不屬於它職責範圍的操作,這就導致了緊耦合。

1.2.2 緊耦合的影響

想像一下我們建立一個這樣的四層模型:

  • 視圖層,應用的界面。它由用戶界面,控件比如按鈕,列表等組成。
  • 表現層,處理 UI 的邏輯部分。它包含了按鈕事件調用的函數,UI 界面列表綁定的存儲數據的對象。
  • 數據訪問層,負責與數據倉庫的交互代碼。數據訪問層知道如何發起一個 Web 服務調用,然後將數據存儲到對象中,以便應用的其他模塊可以方便的使用。
  • 數據倉庫,獲取實際數據的地方。

且四層之間都具有耦合關係。

如果表現層涉及數據上傳,當我們上傳不同的數據格式時,需要不同的數據訪問層邏輯,即數據訪問層對應的數據庫應有所不同。這時,我們就需要在表現層實例化不同的數據訪問層模塊,並通過switch函數來對不同的數據進行不同的數據訪問層處理。這樣看起來沒什麼大問題。如果表現層這時需要添加一個緩衝數據的功能(在網速受限時,提高用戶的使用體驗),你可能又需要在表現層針對緩衝與否,創建不同的數據訪問層對象。這時我們的模塊涉及就違反了單一職責原則,因為我們的表現層不僅處理了UI的邏輯,還兼顧了不同數據格式數據的上傳和決定數據是否使用緩衝。

這樣的代碼難以維護,且運行效率低下,邏輯紊亂。這時候就需要採用解耦的思想來改善代碼。

1.2.3 解耦

解耦的步驟:

  • 添加一個接口,一個抽象層,增加代碼靈活性
  • 在應用程序代碼中加入構造函數注入
  • 將解耦的各個模塊組合到一起

解耦的好處:

  • 解耦合的代碼更加易於擴展。我們能夠在不改變大量對象的情況下增加功能。
  • 我們能夠將功能獨立開來,以便編寫簡短的,易於閱讀的單元測試。
  • 我們也獲得了易於維護的代碼。當程序出錯的時候,我們能夠更加容易發現我們需要修改哪部分內容。
  • 我們在團隊協作開發的過程中,比如提交合併代碼,通常不希望也應該避免團隊成員之間的代碼存在衝突,而解耦合有利於團隊成員各自維護自己的代碼片段而互相不受影響。
  • 解耦合可以使延遲綁定變得更加容易。延遲綁定,或者運行時綁定,是我們在運行時做決定而不是編譯時,這在特定場合下很有用。

1.3 控制反轉(IoC)

1.3.1 依賴注入

Dependency injection(DI)是一個將行為從依賴中分離的技術,簡單地說,它允許開發者定義一個方法函數依賴於外部其他各種交互,而不需要編碼如何獲得這些外部交互的實例。 這樣就在各種組件之間解耦,從而獲得乾淨的代碼,相比依賴的硬編碼, 一個組件只有在運行時才調用其所需要的其他組件,因此在代碼運行時,通過特定的框架或容器,將其所需要的其他依賴組件進行注入,主動推入。

依賴注入可以看成是 反轉控制 (inversion of control)的一個特例。反轉的是依賴,而不是其他。

依賴注入與IoC模式類似工廠模式,是一種解決調用者和被調用者依賴耦合關係的模式它解決了對象之間的依賴關係,使得對象只依賴IoC/DI容器,不再直接相互依賴,實現松耦合,然後在對象創建時,由IoC/DI容器將其依賴的對象注入需要調用的模塊內,最大程度實現松耦合。

如圖2所示,由於引進了中間位置的「第三方」,也就是IoC容器,使得A、B、C、D這4個對象沒有了耦合關係,齒輪之間的傳動全部依靠「第三方」了,全部對象的控制權全部上繳給「第三方」IoC容器,所以,IOC容器成了整個系統的關鍵核心,它起到了一種類似「粘合劑」的作用,把系統中的所有對象粘合在一起發揮作用,如果沒有這個「粘合劑」,對象與對象之間會彼此失去聯繫,這就是有人把IoC容器比喻成「粘合劑」的由來。


圖2

軟件系統在沒有引入IoC容器之前,如果對象A依賴於對象B,那麼對象A在初始化或者運行到某一點的時候,自己必須主動去創建對象B或者使用已經創建的對象B。無論是創建還是使用對象B,控制權都在自己手上,且得負責B的生命周期。
但軟件系統在引入IOC容器之後,這種情形就完全改變了,如圖2所示,由於IoC容器的加入,對象A與對象B之間失去了直接聯繫,所以,當對象A運行到需要對象B的時候,IoC容器會主動創建一個對象B注入到對象A需要的地方。
通過前後的對比,我們不難看出來:對象A獲得依賴對象B的過程,由主動行為變為了被動行為,控制權顛倒過來了,這就是「控制反轉」這個名稱的由來。

1.3.2 IoC的好處

外部存儲通過USB接口與主機相連的例子就很像IoC,當我們需要使用外設進行存儲時,我們只需要將U盤插到USB的接口上就行了,主機需要用通過這個接口對U盤進行讀寫就行。這樣的好處是:

  1. U盤和主機之間只有在相連時才產生關聯,如果雙方出現故障都不會對對方產生影響。這種特性體現在軟件工程中,就是可維護性比較好,非常便於進行單元測試,便於調試程序和診斷故障。代碼中的每一個Class都可以單獨測試,彼此之間互不影響,只要保證自身的功能無誤即可,這就是組件之間低耦合或者無耦合帶來的好處。
  2. U盤廠商不需要根據不同電腦型號生產不同的U盤,兩者只需要遵守統一的USB接口標準。同類似的方式,在軟件開發過程中,每個開發團隊的成員都只需要關心實現自身的業務邏輯,完全不用去關心其它的人工作進展,通過統一的接口規範就可以實現組件的組合,大大加快了開發的速度,也提高了產品的高復用性。
  3. 同USB外部設備一樣,模塊具有熱插拔特性。IOC生成對象的方式轉為外置方式,也就是把對象生成放在配置文件里進行定義,這樣,當我們更換一個實現子類將會變得很簡單,只要修改配置文件就可以了,完全具有熱插撥的特性。

總體而言,控制反轉的出現是為了降低軟件開發工程中不同模塊之間的耦合程度。

2 實例

2.1 沒有使用IoC

下面是一段沒有使用IoC實現模塊之間相互調用的代碼。

/************************************************
* 這是一個書店管理系統
* BookService是管理員使用模塊
* BookService需要讀取數據庫數據,所以必須實例化DataSource對象
************************************************/
public class BookService {
    private DataSource dataSource = new DataSource(config);

    public Book getBook(long bookId) {
        try (Connection conn = dataSource.getConnection()) {
            ...
            return book;
        }
    }
}
/************************************************
* UserService是用戶系統模塊
* 如果UserService現在也要訪問數據庫,就必須實例化DataSource對象
************************************************/
public class UserService {
    private DataSource dataSource = new DataSource(config);

    public User getUser(long userId) {
        try (Connection conn = dataSource.getConnection()) {
            ...
            return user;
        }
    }
}
/************************************************
* CartServlet處理用戶購買
* CartServlet繼承HttpServlet 
* 該類覆寫了deGet()方法
* 並傳入了HttpServletRequest和HttpServletResponse兩個對象,分別代表HTTP請求和響應。
************************************************/
public class CartServlet extends HttpServlet {
    private BookService bookService = new BookService();
    private UserService userService = new UserService();

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        long currentUserId = getFromCookie(req);
        User currentUser = userService.getUser(currentUserId);
        Book book = bookService.getBook(req.getParameter("bookId"));
        //do something else
        ...
    }
}

CartServlet創建了BookService,在創建BookService的過程中,又創建了DataSource組件。這種模式的缺點是,一個組件如果要使用另一個組件,必須先知道如何正確地創建它。

從上面的例子可以看出,如果一個系統有大量的組件,其生命周期和相互之間的依賴關係如果由組件自身來維護,不但大大增加了系統的複雜度,而且會導致組件之間極為緊密的耦合,繼而給測試和維護帶來了極大的困難。

2.2 使用IoC

下面是一段使用IoC實現模塊之間相互調用的代碼。

spring不僅實現了通過容器創建組件,還實現了配置文件內的組件之間的互相調用。

<!--
	這是一個spring容器的配置文件
	文件中指明了3個bean,對應於3個java class
-->
<beans>
    <bean id="dataSource" class="DataSource" />
    <bean id="bookService" class="BookService">
        <property name="dataSource" ref="dataSource" />
    </bean>
    <bean id="userService" class="UserService">
        <property name="dataSource" ref="dataSource" />
    </bean>
</beans>

在IoC模式下,控制權發生了反轉,即從應用程序轉移到了IoC容器,所有組件不再由應用程序自己創建和配置,而是由IoC容器負責,這樣,應用程序只需要直接使用已經創建好並且配置好的組件。為了能讓組件在IoC容器中被「裝配」出來,需要某種「注入」機制,例如,BookService自己並不會創建DataSource,而是等待外部通過setDataSource()方法來注入一個DataSource:

public class BookService {
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}

3 總結

在軟件工程開發的工程中使用依賴注入的手段可以實現不同單元模塊之間的獨立開發和測試,大大提高了開發的效率。同時依賴注入通過容器創建、控制組件,實現了組件之間的松耦合,大大降低了軟件的維護成本。