Dependency Injection - 靜態與依賴
今天到全聯購買了一個體重計,標籤上面寫著 static,售價 500 元。我心想:「不錯,夠便宜,看起來也夠簡單,剛好放在家門口踩一下就知道我今天有沒有胖。」回家之後,我站上去,顯示 66.6kg。
我不滿意。
我開始對著體重計自言自語:
「欸欸,我昨天吃太鹹啦,應該有水腫,不能算胖吧?」
「還有,最近氣壓很低,影響感測精度你懂嗎?」
「而且我最近壓力比較大可能會影響賀爾蒙,體脂肪是不是也該調整一下?」
我認真思考了一下,決定用 Dependency Injection 改造這台體重機。我開始設計以下注入內容:
- IDietHistoryAnalyzer:幫我判斷昨天晚餐的影響,還會取得目前的健康最新趨勢分析
- IPressureSensingService:計算壓力變動
- IWeatherSensorProvider:拉取最新的氣壓與濕度資料
還打算加個 IMoodService,畢竟心情不好看起來也會比較腫,我把所有服務都準備好了,轉頭對體重機說:
「接下來,你要根據這些注入的資料來動態回傳我的理想體重。」
體重機沉默了幾秒,然後…… 它還是顯示了 66.6kg。不只沒變,它還開始顫抖發熱,螢幕出現一行錯誤訊息:
「我只是他媽的一個 500 元的 static 虎克定律工具,拜託不要對我做生命週期控制。」
我才意識到我犯了一個嚴重的錯誤 —— 我只是買了一個用來秤重量的小工具阿!
⏲️ 一個 static 體重計的自白
你說:「我希望體重機幫我考慮最近有沒有水腫、昨晚是不是吃太鹹、天氣氣壓是否影響體重感測。」要注入 IDietHistoryAnalyzer、要加上 IPressureSensingService、還要搭配 IWeatherSensorProvider
體重機崩潰:「我只是一個彈簧機構,你跟我聊水腫幹嘛?」
這個體重機的任務很簡單,他「回報一個數值」,你不能加入其他的模組讓他可以「分析人生健康狀態」。你想要那樣的智慧體重機可以買 5000 元那台,但不能指望一台靜態磅秤變成健康顧問!
⏲️ 為何 static 無法 DI
回到本質上,Static 是什麼?
static 表示這個東西不需要也不能建立物件,他是程式啟動時就存在,全域唯一,共享使用,沒有建構式,無法透過 constructor 注入物件,Static 的生命週期 = 整個應用程式的生命週期
- static 是「全域」的,而依賴注入(Dependency Injection, DI)是「區域」的/「有生命週期」的。
- static 是「無構造函式」static 方法 / 類別 無法 newDI 依賴 constructor injection
- static 方法 無法被 DI container 輸入。
如果想要讓 static 物件可接受外部服務,代換方式是可以在 static method 參數傳入:
1 |
|
這就等於把控制權交回給呼叫方(Caller),讓呼叫方決定要用哪個實例來執行這個方法。保持 static method 無狀態、純函式風格(pure function) 是最好的設計。
有狀態:函式使用或修改了外部的變數(像 static 欄位、global 欄位)函式的行為會根據「之前發生的事情」而改變
無狀態:函式只根據你傳進去的參數做運算 不依賴外部環境 不會修改外部任何東西 呼叫多少次,傳一樣的參數,結果永遠一樣(可預測)
如果你讓 Singleton 去依賴一個 Scoped 或 Transient 的元件,那:Singleton 一直存在,Scoped 可能早就「用完被丟棄」,但 Singleton 還想用它 → 🌋
⏲️ 舊時代轉到 ASP.NET CORE 的做法
在 ASP.NET MVC(非 Core)時代,許多功能都可以直接透過靜態類別(如 MemoryCache.Default 或 HostingEnvironment.MapPath)存取。這樣的設計簡單好用,但到了 ASP.NET Core,這些作法就不再被鼓勵,而是轉向以「依賴注入(Dependency Injection, DI)」為主的架構設計。
以下我們從兩個常見的情境,來看看舊時代與 ASP.NET Core 的使用方式差異。
MemoryCache 的變化
在 ASP.NET MVC (非 Core) 時代,使用記憶體快取很簡單:
1 |
|
MemoryCache.Default 是一個靜態單例,整個網站共用同一份快取,任何地方都能直接存取。
但在 ASP.NET Core 中,這樣的用法已不再適用,若你想使用 MemoryCache,需要透過 依賴注入 的方式來取得快取實體。
✅ ASP.NET Core 的寫法:
注冊服務
1 | public void ConfigureServices(IServiceCollection services) |
注入 IMemoryCache
1 | public class MyController : Controller |
HostingEnvironment 的變化
在舊有架構中,如果你要取得網站的實體路徑,只需要這樣:
1 | var path = HostingEnvironment.MapPath("~/myFolder"); |
這是典型的靜態方法呼叫,無需注入,也無需自行建立物件。
✅ ASP.NET Core 的做法
1 |
|
🌐 IWebHostEnvironment 常見屬性與用途
屬性 / 方法 | 用途說明 |
---|---|
ContentRootPath |
網站根目錄(專案根) — 程式碼、設定檔所在位置 |
WebRootPath |
wwwroot 靜態檔案目錄 — 給前端用的圖片、JS、CSS |
EnvironmentName |
當前執行環境名稱,例如 "Development" 、"Production" 、"Staging" |
IsDevelopment() |
回傳 true 代表目前是開發模式 |
ApplicationName |
專案名稱(組件名稱) |
WebRootFileProvider |
可用來存取 wwwroot 底下的檔案(例如列出圖片) |
ContentRootFileProvider |
存取專案根目錄的檔案 |
❓ 為什麼要改用 DI?
★ 控制生命週期
靜態物件會隨應用程式一啟動就存在,且無法根據需求釋放資源,容易造成記憶體壓力。而透過 DI 註冊的物件,可以選擇適當的生命週期(如 Singleton、Scoped、Transient)。
1 |
|
對靜態物件嘗試依賴注入,本質上就可能會有生命週期互相衝突的問題
★ 增加可測試性
靜態物件如 MemoryCache.Default 是全域共用的,在單元測試中難以替換與控制狀態。
使用靜態快取:
1 | var service = new ProductService(); |
使用 DI 快取
1 | var mock = new Mock<IMemoryCache>(); |
透過 Mock 替代快取物件,讓每次測試都能控制輸入與輸出結果,更穩定、可靠。
★ 支援替換實作
若有一天你想要用 Redis 當作快取,只需要更換 DI 註冊即可:
1 | services.AddSingleton<IMemoryCache, MyCustomRedisCache>(); |
而 ProductService 中的實作完全不用動,只要它依賴的是 IMemoryCache,系統會自動注入你替換後的實作。
☘️ 結語
你無法要求一台 500 元的靜態體重機理解你的心情、分析你的壓力 —— 它本來就只是被設計來「給你一個數字」,而不是「給你一段洞察人生的旅程」。
軟體架構亦是如此。
static 的方式簡單、直覺,適合處理單一職責、無狀態、全域一致的邏輯;但當我們想讓系統變得更聰明、更彈性、更能針對每個情境做出不同反應,就必須學會「注入關係」,建立更有層次、可以擴充與測試的結構。
Dependency Injection 就像是給物件建立了一組社交網絡 —— 它們不再孤立地面對世界,而是從彼此的合作中找到意義與彈性。
你不是不能踩上那台靜態磅秤,但當你準備迎接更複雜、更動態的挑戰時,也別忘了真正能聽懂你話的,不是那台體重機,而是你用 DI 建構出來的那一整套健康顧問系統。