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()来构造类, 限制就是传进来的对象必须存在无参构造函数,一旦有参,这部分编译就会直接报错。如果方法本身需要参数,这部分就只能走构造函数最终变得非常臃肿。