EnriqueMark
設計模式與架構

控制反轉

2026-04 → 2026-09 更新 繁體中文

不懂IoC只會把DI用成臃腫的工廠模式——比如XML,JSON,TXT的格式轉換,在裡面new了一堆內容,並用列舉switch來判斷——但完全可以IoC。有些人說IoC才是DI的精髓,我看也不是沒有道理。控制,本質上說的就是「我要用的東西由誰來構造」。比較簡單的訊號是看我如果要用一個物件,這個東西是誰new的,如果是我自己new的,那控制權就在我自己手中,但如果是外部遞給我的,那就是IoC。

比如下面的程式碼,就是雖然注入了,控制權卻還是內部的——外面通過列舉來控制轉換方法的內部行為,這種情況下看上去好像是控制權在外部,但事實上這是一種間接的偽控制,真正的控制權仍然在內部,你只是給了一個切換訊號而已。

// 注入了,但控制沒有反轉
class ConversionService {
    public string Convert(string input, FormatEnum format) {
        switch (format) {
            case FormatEnum.XML:  return new XmlConverter().Convert(input);
            case FormatEnum.JSON: return new JsonConverter().Convert(input);
            // 每加一種格式就改這裡——開閉原則直接破功
        }
    }
}

而下面才是真正的IoC,方法體只對結構負責,所需要的物件完全交給外部構建,控制權完全反轉。

interface IConverter {
    string Convert(string input);
}
  
class ConversionService {
    private readonly IConverter _converter;

    // 依赖由外部注入,Service 本身不负责 new
    public ConversionService(IConverter converter) {
        _converter = converter;
    }

    public string Convert(string input) {
        return _converter.Convert(input);
    }
}

// 呼叫方(控制权在外部)
IConverter xmlConverter = new XmlConverter(); 
var svc = new ConversionService(xmlConverter); // 注入依赖

我覺得這裡有一點很重要,那就是遙控和外部注入看上去都是控制,但卻有一個至關重要的不同。前者是選擇,邊界本質上是方法內部給定的,你能做的就是在已有的幾個選項裡面選,實際上控制權仍然在內部。而IoC則是徹底的放手:不關心你給的是什麼,也不關心邊界,只對介面(結構)負責。也就是説,XML,JSON,TXT這是三個,遙控也只能選擇這三個;而IoC就是無限,只要你合規,那傳什麼都行——這才是真正的控制反轉,只對介面負責的放手。

如果用廚房和廚師來比喻的話,前者就是廚房自帶廚師,但只有三位:川菜、湘菜、本幫菜。那我想吃河南菜呢?不好意思,沒有。而IoC就是:提供廚房,廚師自備。河南菜?沒問題。東北菜?沒問題。西餐?當然沒問題——但前提是用中式廚具,畢竟廚房(介面)是固定的。總之,控制反轉的關鍵就在此,提供了一種最大的靈活性和抽象,但也不是毫無約束的:定義介面至關重要。

但是這個地方有個坑,那就是把泛型當作IoC,覺得自己控制反轉了,但實際上控制權還在內部:

class ConversionService<T> where T : IConverter, new()
{
    public string Convert(string input)
    {
        var converter = new T();   // 型別由外部決定,但構造仍然在内部
        return converter.Convert(input);
    }
}
var svc = new ConversionService<XmlConverter>();

型別從外面注入的確解決了列舉版本每次都要修改主邏輯的臃腫問題,也讓這部分抽象出來了,但 new 這個操作仍然是發生在內部,所以控制權仍然是內部的——此處沒有反轉。並且這個地方還有個坑,那就是用 new()來構造類, 限制就是傳進來的物件必須存在無參建構函式,一旦有參,這部分編譯就會直接報錯。如果方法本身需要引數,這部分就只能走建構函式最終變得非常臃腫。