Image


今天到全聯購買了一個體重計,標籤上面寫著 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
2
3
4
5
6
7
8

public static class WalletHelper
{
public static async Task<WalletBalance> GetBalanceAsync(IApiClients apiClients)
{
return await apiClients.BalanceAsync(new WalletRequest());
}
}

這就等於把控制權交回給呼叫方(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
2
3

var cache = MemoryCache.Default;

MemoryCache.Default 是一個靜態單例,整個網站共用同一份快取,任何地方都能直接存取。

但在 ASP.NET Core 中,這樣的用法已不再適用,若你想使用 MemoryCache,需要透過 依賴注入 的方式來取得快取實體。

✅ ASP.NET Core 的寫法:

注冊服務

1
2
3
4
public void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache();
}

注入 IMemoryCache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyController : Controller
{
private readonly IMemoryCache _cache;

public MyController(IMemoryCache memoryCache)
{
_cache = memoryCache;
}

public IActionResult Index()
{
_cache.Set("key", "value");
return View();
}
}


HostingEnvironment 的變化

在舊有架構中,如果你要取得網站的實體路徑,只需要這樣:

1
var path = HostingEnvironment.MapPath("~/myFolder");

這是典型的靜態方法呼叫,無需注入,也無需自行建立物件。

✅ ASP.NET Core 的做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

public class MyClass
{
private readonly IWebHostEnvironment _env;

public MyClass(IWebHostEnvironment env)
{
_env = env;
}

public string GetPath()
{
return Path.Combine(_env.ContentRootPath, "myFolder");
}
}

🌐 IWebHostEnvironment 常見屬性與用途

Image

屬性 / 方法 用途說明
ContentRootPath 網站根目錄(專案根) — 程式碼、設定檔所在位置
WebRootPath wwwroot 靜態檔案目錄 — 給前端用的圖片、JS、CSS
EnvironmentName 當前執行環境名稱,例如 "Development""Production""Staging"
IsDevelopment() 回傳 true 代表目前是開發模式
ApplicationName 專案名稱(組件名稱)
WebRootFileProvider 可用來存取 wwwroot 底下的檔案(例如列出圖片)
ContentRootFileProvider 存取專案根目錄的檔案


❓ 為什麼要改用 DI?


★ 控制生命週期

靜態物件會隨應用程式一啟動就存在,且無法根據需求釋放資源,容易造成記憶體壓力。而透過 DI 註冊的物件,可以選擇適當的生命週期(如 Singleton、Scoped、Transient)。

1
2
3
4
5

services.AddSingleton<ILogger, MyLogger>(); // 記錄整體流程,無需重複建立;
services.AddScoped<IDbContext, MyDbContext>(); // 在一個請求中共用,確保交易一致性;
services.AddTransient<INotificationService, EmailSender>(); // 屬於一次性操作,不需保留狀態。

對靜態物件嘗試依賴注入,本質上就可能會有生命週期互相衝突的問題



★ 增加可測試性

靜態物件如 MemoryCache.Default 是全域共用的,在單元測試中難以替換與控制狀態。

使用靜態快取:

1
2
3
var service = new ProductService();
Assert.Equal("..."); // 前一次試算效應會影響這次

使用 DI 快取

1
2
3
4
5
6
var mock = new Mock<IMemoryCache>();
object value = null;
mock.Setup(m => m.TryGetValue("product", out value)).Returns(false);

var service = new ProductService(mock.Object);
Assert.Equal("從資料庫查出來的商品", service.GetProduct());

透過 Mock 替代快取物件,讓每次測試都能控制輸入與輸出結果,更穩定、可靠。



★ 支援替換實作

若有一天你想要用 Redis 當作快取,只需要更換 DI 註冊即可:

1
services.AddSingleton<IMemoryCache, MyCustomRedisCache>();

而 ProductService 中的實作完全不用動,只要它依賴的是 IMemoryCache,系統會自動注入你替換後的實作。

☘️ 結語

你無法要求一台 500 元的靜態體重機理解你的心情、分析你的壓力 —— 它本來就只是被設計來「給你一個數字」,而不是「給你一段洞察人生的旅程」。

軟體架構亦是如此。

static 的方式簡單、直覺,適合處理單一職責、無狀態、全域一致的邏輯;但當我們想讓系統變得更聰明、更彈性、更能針對每個情境做出不同反應,就必須學會「注入關係」,建立更有層次、可以擴充與測試的結構。

Dependency Injection 就像是給物件建立了一組社交網絡 —— 它們不再孤立地面對世界,而是從彼此的合作中找到意義與彈性。

你不是不能踩上那台靜態磅秤,但當你準備迎接更複雜、更動態的挑戰時,也別忘了真正能聽懂你話的,不是那台體重機,而是你用 DI 建構出來的那一整套健康顧問系統。