撰寫程式,總是得相依在某些元件上吧!就算是個簡單的 Hello, World 程式,你就得相依在 System
、String
等類別,當然,這些是標準 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 類別,因為僅僅是相依在 AccountDAO
、MessageDAO
的行為,那麼應用程式就能夠控制,未來是要採用哪種實現類別了。
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 寫的書那樣設定。