網頁

2019/8/16

Java 什麼是依賴注入(Dependency Injection)

本篇說明什麼是依賴注入(Dependency Injection),簡稱DI。


在了解什麼是依賴注入之前先了解依賴(dependency)是什麼意思。

就「依賴」的文字意思來看,是指「一個東西需要另一個東西而存在;若沒有另外一個東西,則本身不能自立」。

例如人類生存依賴的基本要素「陽光」、「空氣」、「水」,意思就是人類需要這三種東西才能生存,換句話說,就是人類沒有這三種東西則無法生存。所以人類對這三種東西有了依賴。


什麼是依賴

而在物件導向程式(OOP)中,程式是透過許多類別(Class)的實例(instance),也就是物件(object),彼此的交互組合來實現各種功能。物件導向程式的依賴是指「一個物件需要另一個物件才能作用」。

例如購物網站的購物車在使用者結帳(checkout)後要出貨,要實現此功能可能有兩個類別ShoppingCart(購物車)與PostShipping(郵局貨運)。

在購物車的結帳方法checkout()中會先建立一個實現出貨邏輯的PostShipping物件並呼叫其shipOrder()方法來出貨,此時兩個物件便發生了依賴關係,也就是ShoppingCart依賴PostShipping

ShoppingCart

public class ShoppingCart {
    
    private void checkout() {
        ...
        PostShipping postShipping = new PostShipping();
        postShipping.shipOrder(order);
        ...
    }

}

所以ShoppingCart為依賴對象,PostShipping為被依賴對象。

以UML類別圖表示如下:



也就是說ShoppingCart若要實現結帳checkout()的功能必須依賴PostShippingshipOrder(),即ShoppingCartPostShipping有依賴。以上即為OOP的dependency。


什麼是依賴注入

那什麼是依賴注入(Dependency Inection)呢?從上例可看到ShoppingCart使用被依賴的PostShipping時,是在自身的程式中透過new產生:

PostShipping postShipping = new PostShipping();

依賴注入是指「被依賴物件透過外部注入至依賴物件的程式中使用」,也就是被依賴物件並不是在依賴物件的程式中使用new產生,而是從外部「注入(inject)」至依賴物件。

依賴注入可以透過類別的建構式(Constructor),Setter來實現,被依賴物件可從依賴物件的建構式或Setter傳入。

public class ShoppingCart {
    
    private PostShipping postShipping;
    
    // 由建構式從外部注入被依賴的物件至依賴的程式中 (constructor injection)
    public ShoppingCart(PostShipping postShipping) { 
        this.postShipping = postShipping;
    }
    
    private void checkout() {
        postShipping.shipOrder(order);
    }

    // 由Setter從外部注入被依賴的物件至依賴的程式中 (setter injection)
    public void setPostShipping(PostShipping postShipping) { 
        this.postShipping = postShipping;
    }

}


為什麼用依賴注入

為什麼要用依賴注入呢?依賴注入有什麼好處?解決了什麼問題?簡單說「依賴注入是為了解決物件間高耦合的問題」。

高耦合(High coupling)是指當一個被依賴對象被修改時會連帶影響依賴對象也得修改。

如果被依賴對象是在依賴對象中產生,也就是在依賴對象中使用new來產生被依賴物件時,就會出現高耦合的問題,因為一旦被依賴物件的生成方式需要被修改或想要替換被依賴對象時,則依賴對象的業務邏輯程式也必須同時修改。而依賴注入則隔離了依賴對象原有的業務邏輯與被依賴對象的生成邏輯,所以可降低依賴對象與被依賴對象間的耦合,因此提高程式的可維護性。


例如運送類別要由PostShipping(郵局貨運)改為HctShipping(新竹貨運),如果沒有使用依賴注入,那程式中每個用new產生的PostShipping都要改成HctShipping。更糟的是如果建構HctShipping物件的邏輯很複雜,那修改起來就會非常痛苦,這就是高耦合造成的問題。

ShoppingCart

public class ShoppingCart {
    
    private void checkout() {
        ...
        HctShipping hctShipping = new HctShipping(); // <-- 程式中每個 new PostShipping() 都要改成 new HctShipping()
        hctShipping.shipOrder(order);
        ...
    }

}

為了解決以上問題,可以透過介面(interface)與依賴注入的方式來降低依賴間的耦合,這樣的作法又稱為「依賴倒置/依賴反轉(Dependency Inversion Principle, DIP)」。

定義一個Shipping介面即通用的方法名稱,並由具體的運送類別繼承並實作。

/** 運送介面 */
public interface Shipping {
    
    public void shipOrder(Order order);

}

/** 郵局貨運 */
public class PostShipping implements Shipping {

    @Override
    public void shipOrder(Order order) { ... }

}

/** 新竹貨運 */
public class HctShipping implements Shipping {

    @Override
    public void shipOrder(Order order) { ... }

}

然後在依賴對象改以介面(介面隔離原則)來操作被依賴對象的方法,並透過依賴注入的方式由外部注入依賴對象的實例。

ShoppingCart

public class ShoppingCart {
    
    private Shipping shipping; // <-- 改為Shipping介面,此為利用物建導向的多型特性。
    
    public ShoppingCart(Shipping shipping) {
        this.shipping = shipping; // 被依賴物件由外部注入,達到業務邏輯與被依賴物件的生成邏輯分開
    }
    
    private void checkout(Order order) {
        shipping.shipOrder(order);
    }

}

以UML圖表示如下。



如此修改運送類別時就不會影響ShippingCart本身結帳的業務邏輯,只要修改外部物件的生成邏輯。

依賴注入帶來的另個好處是撰寫單元測試(Unit Test)變得容易。由於被測對象的依賴是從外部注入,所以單元測試時就能利用依賴注入與介面隔離輕鬆替換真實物件為mock物件,並對mock物件做stubbing等手法來隔離測試對象與依賴對象間的邏輯達到單元測試目的。


Java DI套件

而在Java生態系中最著名的依賴注入的框架就是Spring Core的控制反轉容器Inversion of Control(IoC) Container,其為依賴注入的實現框架,負責提供程式中物件的生成、初始化與裝配等工作。Spring IoC藉由修改配置(configuration)的方式讓我們可以輕鬆替換程式中物件間的依賴對象而不用修改程式本身。

其他Java的DI框架還有DaggerGoogle Guice


若文章對您有幫助還幫忙點個廣告,謝謝支持。


4 則留言:

  1. 版主您好

    想請教您:
    如果今天透過注入注入 private Shipping shipping; 時
    想使用的是另一個 HctShipping 這個實現類呢?

    只能依賴 Guice 或 spring 等框架指定名稱達到目的嗎?

    回覆刪除
  2. 樓上您好,不用一定要用框架也可,只要在建購ShoppingCart時傳入(注入)HctShipping的實例即可,即new ShoppingCart(new HctShipping),又稱建構式注入(constructor injection)。

    private Shipping shipping成員型態為介面,可以賦予實作類PostShipping或HctShipping。建議您先了解什麼是多型後應該就可以理解。

    回覆刪除
  3. 謝謝版主的回復
    總結來說要用使用 HctShipping 的話
    如下方的寫法就可以使用嚕?

    public class Controller {

    private Shipping shipping;

    public Controller(Shipping shipping) {
    this.shipping = shipping;
    }

    private void doSomething() {
    new ShoppingCart(new HctShipping()).shipping.checkout(order);
    }
    }

    回覆刪除
  4. Hi 樓上,應該是這樣:
    public class Controller {

    private ShoppingCart shoppingCart;

    public Controller() {
    this.shoppingCart = new ShoppingCart(new HctShipping());
    }

    private ship(Order order) {
    shoppingCart.checkout(order);
    }
    }

    回覆刪除