---
title: "控制反转"
date: "2026-04"
category: "设计模式与架构"
tags: ["设计模式与架构"]
description: "不懂ioc只会把DI用成臃肿的工厂模式——比如XML,JSON,TXT的格式转换，在里面new了一堆内容，并用枚举switch来判断——但完全可以ioc，定义一个泛型，让外部给入..."
source: "https://enriquemark.com/zh-hans/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一样，不到运行的时候你是不知道会不会报错的。这也是灵活性和稳健性的权衡，在有些场景下确实需要灵活性，但必须明确的知道其代价和边界。如果可以的话，提前在这种灵活性上做好错误封装：就算结果不可预料，也要控制损失范围，使其尽可能最小化。
