Dependency Injection


撰寫程式,總是得相依在某些元件上吧!就算是個簡單的 Hello, World 程式,你就得相依在 SystemString 等類別,當然,這些是標準 API,撰寫程式總是得基於一些標準 API,然而,即便如此,相依於標準 API 這件事實並不會改變。

如果你直接使用 java.util.logging,相依於 java.util.logging 這件事情就發生了,有人可是很不滿意 java.util.logging,未來若真的要改用其他日誌程式庫,你就得修改程式碼中,有使用到 java.util.logging 的部份,你用的越多,要修改的越多。

去除相依是個古老的議題,留下了許多的經驗,像是設計模式,有各種的手法,像是重構,以及更高層次的原則等,許多程式庫的版本發展,也記錄了去除相依性的過程。

(看看 Java 日誌程式庫的歷史發展就是如此,有興趣瞭解的話,建議看一下〈哪來這麼多日誌程式庫?〉。)

相依並不完全是不好的,畢竟完全沒有相依,是不可能組合出應用程式的,不好的是,相依在不必要的細節,當細節充斥在應用程式中各個角落,未來有關於細節的任何變動,就得找出這些角落來逐一修改,細節控制了你的應用程式,細節一但變動,應用程式就會跟著變動

有哪些是細節呢?檔案存取是細節,如果你使用了檔案作為資料永續(Persistence)的實現方案,直接在應用程式中隨處撰寫檔案處理的相關 API,未來若因故必須改為資料庫,勢必就得逐一找出各個運用到檔案處理相關 API 的角度,然後逐一修改,當然,資料庫也是個細節,未來有沒有可能改用別的永續(Persistence)方案呢?

你應該辨識出應用程式真正的資料永續行為有哪些,讓應用程式依賴在這些行為上,而不是依賴在實現細節上,如此應用程式在挑選、抽換實現細節上就會有彈性,應用程式就能夠控制細節

例如,像《Servlet&JSP 技術手冊 -從 Servlet 到 Spring Boot》第七章的 UserService 類別,裏頭直接撰寫了 NIO2 等 API,NIO2 的細節就控制了 UserService,相關的細節變動,都會導致 UserService 的變動。

如果重構為第八章的 UserService 類別,因為僅僅是相依在 AccountDAOMessageDAO 的行為,那麼應用程式就能夠控制,未來是要採用哪種實現類別了。

Spring 的 Inversion of Control,基本上指的就是這樣的概念,也就是相依的反轉(Dependency Inversion),或者更具體地,不讓相依的細節控制應用程式,而是應用程式控制細節的選擇,這樣的概念展現在 Spring 的各個實現上。

某些程度上,其實真的就是那些古老的觀念,像是「高層模組不應該依賴低層模組,而是模組都必須依賴於抽象」、「實現必須依賴抽象,而不是抽象依賴實現」等。

談到行為定義,談到抽象,Java 中常見使用介面,不過,並非用了介面就是好事,還得看你怎麼應用,有不少的程式庫或框架,會要求應用程式得實作某些介面,以便符合應用程式期待的某些約定,這就使得框架的 API 侵入至應用程式,視程式庫或框架的設計而定,侵入的情況可能是難以接受,並導致應用程式維護上的麻煩。

Spring 開始發跡的那個年代的 EJB,就是這類的情況,例如,你得實現 EntityBean 介面來實現一個 EntiyBean,這樣 EJB 容器才能將 EntityContext,透過 setEntityContext 方法注入給你的元件:

...
import javax.ejb.*;

public class EmployeeBean implements EntityBean {
    private EntityContext ctx;

    public void setEntityContext(EntityContext ctx) {
        this.ctx = ctx;
    }

    public void ejbStore() {
        ...
    }

    public void ejbLoad() {
        ...
    }

    public void ejbRemove() {
        ...
    }

    public void ejbActivate() {
        ...
    }

    public void ejbPassivate() {
        ...
    }
}

這就使得 EmployeeBean 相依於 EJB 容器了,你用到越多 EJB 容器的服務,這類的相依性就越多;這就好比我開發了一個使用者服務容器,為了要能注入 UserService 至你的元件之中,強制你得實現某個介面:

public class Customer implements IService {
    private UserService service;

    public void setUserService(UserService service) {
        this.service = service;
    }

    ...
}

Spring 主張「應用程式不應依賴於容器,而是容器服務於應用程式」,就目前的 Spring 版本來說,若基於標註(Annotation),可以簡單地如下注入 UserService

@Component
public class Customer {
    @Autowired
    private UserService service;

    public void setUserService(UserService service) {
        this.service = service;
    }

    ...
}

實際上 setUserService 也不是必要的,視需求而定,你可以決定要不要留下 setUserService 方法。你只要遵照 Spring 的規範,適當地完成設定以及初始相關資源,Spring 就會自動建立 UserService 實例,並設定給 Customer,這是 Spring 提供的相依注入(Dependency Injection)服務。

在非 Spring 的環境中,Customer 是中性的,只要忽略標註,它完全可以當成純綷的 Java 物件來使用,這對於測試上也有很大的幫助,你不必費心準備與容器相關的資源。

Spring 鼓勵你基於標註來進行設定,若真的必要,也可以不使用標註,採用 XML 的方式,就像我過去針對 Spring 1.2.5 寫的文件,或者是針對 Spring 2.0 寫的書那樣設定。