EnriqueMark
设计模式与架构

控制反转

2026-04 简体中文

不懂ioc只会把DI用成臃肿的工厂模式——比如XML,JSON,TXT的格式转换,在里面new了一堆内容,并用枚举switch来判断——但完全可以ioc,定义一个泛型,让外部给入,在里面new T——完全不需要枚举。有些人说ioc才是DI的精髓,我看也不是没有道理。

比如下面的代码,就是虽然注入了,控制权却还是内部的——外面通过枚举来控制转换方法的内部行为,这种情况下看上去好像是控制权在外部,但事实上这是一种间接的伪控制,真正的控制权仍然在内部,你只是给了一个切换信号而已。

// 注入了,但控制沒有反轉
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<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 ,那好得是规定了接口的结构到底啥样,如果写的有问题,编译期间就会报错。而反射的话,我只能进行类型转换——那就是我和你约定,这里需要是这个类型,但你到底是不是真用这个类型,我是没法硬性限制的。

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

比如这里,万一给进来的就是不符合约定,那只有运行的时候才知道错误。但如果我用的是泛型,那编译期间就会直接报错提示,根本就不会到最后阶段。因此反射其实多少有了点动态语言的味道——就跟python一样,不到运行的时候你是不知道会不会报错的。这也是灵活性和稳健性的权衡,在有些场景下确实需要灵活性,但必须明确的知道其代价和边界。如果可以的话,提前在这种灵活性上做好错误封装:就算结果不可预料,也要控制损失范围,使其尽可能最小化。