---
title: "控制反轉"
date: "2026-04"
category: "設計模式與架構"
tags: ["設計模式與架構"]
description: "不懂ioc只會把DI用成臃腫的工廠模式——比如XML,JSON,TXT的格式轉換，在裡面new了一堆內容，並用列舉switch來判斷——但完全可以ioc，定義一個泛型，讓外部給入..."
source: "https://enriquemark.com/zh-hant/posts/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%89"
---

不懂ioc只會把DI用成臃腫的工廠模式——比如XML,JSON,TXT的格式轉換，在裡面new了一堆內容，並用列舉switch來判斷——但完全可以ioc，定義一個泛型，讓外部給入，在裡面new T——完全不需要列舉。有些人說ioc才是DI的精髓，我看也不是沒有道理。

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

```csharp
// 注入了，但控制沒有反轉
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，控制權完全反轉。

```csharp
interface IConverter {
    string Convert(string input);
}

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>();
```

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

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

而這裡也能看出來介面設計的重要性到底在哪裡。如果給的太寬，那就是啥都能行，結果也是行為不可預料，反而造成了一種耦合。這裡的問題其實和繼承一樣，那就是存在隱式依賴問題。你根本不知道子類用了啥，也許基類改了個無關緊要的東西，結果下面炸了。因此，介面設計需要的是在靈活性和約束之間權衡，一個良好的介面必須約定明確且邊界清晰。否則模糊的介面設計之下，IoC也救不了你的耦合。

而還有一種更極緻的用法是跟反射結合，在編譯期都可以完全不指定型別。但這裡有代價，如果我寫 `where T : IConverter` ，那好得是規定了介面的結構到底啥樣，如果寫的有問題，編譯期間就會報錯。而反射的話，我只能進行型別轉換——那就是我和你約定，這裡需要是這個型別，但你到底是不是真用這個型別，我是沒法硬性限制的。

```C#
Type type = Type.GetType("MyApp.XmlConverter");
IConverter converter = (IConverter)Activator.CreateInstance(type);
// 如果 XmlConverter 根本沒實現 IConverter，這裡直接 InvalidCastException
```

比如這裡，萬一給進來的就是不符合約定，那只有執行的時候才知道錯誤。但如果我用的是泛型，那編譯期間就會直接報錯提示，根本就不會到最後階段。因此反射其實多少有了點動態語言的味道——就跟python一樣，不到執行的時候你是不知道會不會報錯的。這也是靈活性和穩健性的權衡，在有些場景下確實需要靈活性，但必須明確的知道其代價和邊界。如果可以的話，提前在這種靈活性上做好錯誤封裝：就算結果不可預料，也要控制損失範圍，使其盡可能最小化。
