EnriqueMark
設計模式與架構

依賴注入

2026-03 繁體中文

依賴注入(DI)真的蠻有意思的,本質上來說就是一種更加抽象的封裝,把整個類變得更加通用化或單純化。比如我需要一個轉換器,比起把那些資料直接寫死在裡面,不如從外面傳入——但這裡還只是資料,如果傳入的是函式或者類呢?這就是為什麼如果我本身要採取依賴注入的模式需要先定義一個介面,我需要定義傳入的物件結構體和輸出我才好對它進行後續的處置。但這一部已經相當通用和靈活了。介面的的實現可以是任何模式,只需要使用的時候傳入即可。一個轉化器寫死資料的時候只能特化於特定的內容,但如果是依賴DI,就可以轉換任何內容,只要其實現了我的介面。

這裡的典型運用就是.net對服務的注入實現,比如控制器的初始化邏輯裡面接受一個服務的元素,而我不關心他從哪裡來怎麼實現,我只假設它是存在的,然後使用它。後續這個服務依賴是從外部「傳」或者說「注入進來的」。這裡的本質是使用和構造的分離。怎麼構造是你的事,而我只假設其存在並繼續進行我的邏輯。而不是所有的都寫在一起,造成程式碼高度耦合,彼此難以分離。

這裡更進一步,就是「依賴倒置」。比如上一步,傳進來的可能還是個具體實現的類——但是,如果我傳進來的不是類,而是介面呢?那麼底層邏輯和高層邏輯在這裡就解耦了,這就是為什麼就是加上依賴倒置的DI才是完全體。因為當我的注入仍然依賴於一個具體的實現類時,這裡的抽象層級仍然不夠高。但如果我依賴的是一個抽象的介面,那麼底層如何實現就真的和高層邏輯無關了。高層只關注介面的定義的結構,而底層可以隨便增加或變換,都可以一直用這一個邏輯不變應萬變。或者說,以不變應萬變的基礎,便是抽象。但是,抽象的代價也很簡單,如果我的底層真的就只用這一個邏輯,也沒什麼擴充需求,幹什麼要額外寫個介面再多寫一個實現介面的繁瑣過程呢?所以如果不復用,那就沒必要額外增加設計開銷,這本身是過度設計。

這裡有個問題,為什麼我不直接在一個函式的初始化裡new這個類,然後作為整個類的屬性,是不是也可以實現一整個類直接使用它?為什麼一定要注入呢?其實就是結構和具體的區別。如果這麼做,那整個程式碼就和這個具體的實現綁死了。並且如果碰上生命週期管理很短的高層類,每次請求都new一個新服務,那麼若是這個服務裡面有什麼誇請求的資料共享,這就全沒了。這種情況下最好的做法就是把服務改成單例模式——並且,別忘了前面說過的,如果真是直接new服務,那麼高層和底層本質上還是耦合的,依賴倒置要求介面,但你打算在控制器裡重寫服務嗎?介面可沒辦法new。

依賴注入本身是一種抽象。它對結構負責,被注入物件假設注入物件的結構,並展開邏輯,而new則是對特定的實現負責,連帶結構之內的血肉也包含在內了。一個結構可以有多種具體,就像人一樣,都是四肢五官,但卻千奇百怪,但如果我說只能是某個具體的人,這就一下子縮限了。

高階函式就是一種函式級別的DI,不關注具體實現,只關注函式本身,但是由於拍什麼動態性質函式的結構體有時是未定義的,導致處理起來需要嚴格宣告函式式什麼樣,但沒辦法嚴格在編譯時規定。而python這樣的動態語言 只能靠寫約定,而沒法在編譯層面定死結構。

一般情況下,依賴注入都需要搭配控制反轉