2019年1月14日 星期一

Dependency Injection 與 static

最近一直在思考 Spring 與 static 之間的關係。一般來說,Spring 會管理由它生成的 Bean,不過如果類別本身是以 static 形式呼叫(例如程式碼呼叫 MyClass.callStaticMethod() 這種方式)的話,正常來說就不會被 Spring 所管理,自然在類別內的自動注入(如果有的話)也不會生效。雖然是有一些小技巧可以姑且繞過問題,不過感覺上從 Spring 的角度,好像應該用別種思考模式來思考這個問題。

關於這個問題,在請教 qrtt1 大神之後獲得了解惑,請大家一起膜拜 qrtt1 大神~(拜)。不過其實概念也並不困難,只是不知道為什麼原本一直想不通。

Dependency Injection

Spring 是一個大量使用 Dependency Injection 的框架,而為什麼我們需要 Dependency Injection 呢?簡要來說,假設我們有 A 類別和 B 類別,當我們在 B 類別中要呼叫 A 類別的時候,我們並不希望 B 類別必須了解 A 類別的資訊,例如 B 類別不需要知道 A 類別要如何初始化,因為這樣會造成一些不必要的困擾。舉例來說,如果 A 類別是個存取資料庫的類別,B 類別期待的是呼叫 A 類別就能夠完成資料庫存取,而不需要先在 B 類別裡透過像是 new A(“127.0.0.1”); 這樣的程式碼來初始化 A 類別,造成 B 類別必須知道它不需要知道的事情(資料庫的具體位置等)。

比較詳盡的描述其實需要引入不少其他的名詞和訊息,因此推薦可以參考 [1],這裡暫且先避開細節。

當我們透過類似注入的手法,由外部將 A 類別傳入 B 類別時,我們就能夠避開必須讓 B 類別知道 A 類別的細節的狀況。而再更上一層來說,我們可能想要更進一步抽象化 A 類別,例如 A 類別其實是個 abstract class、或者是個介面,如此一來 B 類別甚至不需要知道 A 類別具體是什麼東西。以 A 是資料庫存取的物件來說,對 B 類別而言 A 就是個能夠存取資料庫的工具,但 A 是什麼資料庫其實對 B 類別無關緊要。

Dependency Injection 這樣的技術,就是為了這個目的而存在的。透過 Dependency Injection,我們可以隱藏 dependency(也就是 A)的初始化過程,把 dependency 注入到目標裡,以達到「解耦」的目的。其中,解耦實際上並非是真正的最終目的,真正的最終目的是要能夠很簡單地抽換 dependency,而要能夠做到簡單地抽換,我們必須把兩者的緊密度盡可能降低,讓雙方互相對對方細節的了解越少越好,因此需要「解耦」。

static 的本質

當我們使用 static 的時候,意味著這個東西是在編譯期間就已經被決定好了,我們不太容易在不直接變更它的原始碼的情況下,在執行期間把它置換掉。換言之,這表示當我們因為任何原因想要更換 static 元件,大多會需要複雜的邏輯或者過程,並且當然也會花費大量的開發時間。

Dependency Injection 與 static

接著,我們同時考慮 Dependency Injection 和 static 時,雖然我們可以透過某些手法,強行在類別中「注入」static 物件,但實務上應該要這麼做嗎?而如果類別操作的是 static method 的話呢?

就我目前的理解,static 在一定程度上是違反 Dependency Injection 的概念的。因為當我們在類別中操作 static 物件時,通常代表了這個物件在執行階段想要被替換是很困難的。不過講到這裡,本來我自己會有的疑惑就是~為什麼我會想要替換它呢?如果是採取 Singleton Pattern 時,對於資料庫連線這種物件,我應該根本不會想要替換它?不過實務上是有除了「未來不知道會不會發生的擴充」這個理由以外更近在眼前的需求:單元測試。

在進行單元測試時,有一些基本的準則需要遵守,不過因為這跟這個主題的關聯性較低,所以這裡也不贅述。如果想要了解細節的話,誠心推薦單元測試的藝術 [2] 這本書!大體上就是單元測試應該要又快又簡單。但要達到這個目的,實務上會有很多問題,例如如果我的程式碼需要存取資料庫時該怎麼辦?如果沒有什麼特殊的技巧的話,大概就是選擇弄一個測試資料庫來做測試。但這會造成很多問題,例如我們必須維護一個用來進行單元測試的測試資料庫等等,而且如果類別用到的是像是金流服務這種我無法自己建測試環境的東西呢?因此實際上我們真正想要的,是一個假的 dependency,也就是假的資料庫、假的金流服務等等。這就繞回我們一開始談的 Dependency Injection 的目的:我們想要能隨時抽換 dependency,像是在進行單元測試時,我們就會想要做一個假的 dependency 來方便單元測試的進行。而這時 static 就會造成阻礙,因為 static 很難在執行期間被替換。

綜上所述,雖然 static 並非完全不能使用,但實務上運用是需要多加思量一下。而這同時也表示使用 Singleton Pattern 時也同樣需要多加思量。

參考資料
  1. 控制反轉 (IoC) 與 依賴注入 (DI)
  2. 單元測試的藝術
  3. What is so bad about singletons?
  4. JCConf 2018 R3-3 / Improving your Test Driven Development / Jakub Nabrdalik

沒有留言: