EnriqueMark
设计模式与架构

依赖注入

2026-03 简体中文

依赖注入(DI)真的蛮有意思的,本质上来说就是一种更加抽象的封装,把整个类变得更加通用化或单纯化。比如我需要一个转换器,比起把那些数据直接写死在里面,不如从外面传入——但这里还只是数据,如果传入的是函数或者类呢?这就是为什么如果我本身要采取依赖注入的模式需要先定义一个接口,我需要定义传入的对象结构体和输出我才好对它进行后续的处置。但这一部已经相当通用和灵活了。接口的的实现可以是任何模式,只需要使用的时候传入即可。一个转化器写死数据的时候只能特化于特定的内容,但如果是依赖DI,就可以转换任何内容,只要其实现了我的接口。

这里的典型运用就是.net对服务的注入实现,比如控制器的初始化逻辑里面接受一个服务的元素,而我不关心他从哪里来怎么实现,我只假设它是存在的,然后使用它。后续这个服务依赖是从外部「传」或者说「注入进来的」。这里的本质是使用和构造的分离。怎么构造是你的事,而我只假设其存在并继续进行我的逻辑。而不是所有的都写在一起,造成代码高度耦合,彼此难以分离。

这里更进一步,就是「依赖倒置」。比如上一步,传进来的可能还是个具体实现的类——但是,如果我传进来的不是类,而是接口呢?那么底层逻辑和高层逻辑在这里就解耦了,这就是为什么就是加上依赖倒置的DI才是完全体。因为当我的注入仍然依赖于一个具体的实现类时,这里的抽象层级仍然不够高。但如果我依赖的是一个抽象的接口,那么底层如何实现就真的和高层逻辑无关了。高层只关注接口的定义的结构,而底层可以随便增加或变换,都可以一直用这一个逻辑不变应万变。或者说,以不变应万变的基础,便是抽象。但是,抽象的代价也很简单,如果我的底层真的就只用这一个逻辑,也没什么扩充需求,干什么要额外写个接口再多写一个实现接口的繁琐过程呢?所以如果不复用,那就没必要额外增加设计开销,这本身是过度设计。

这里有个问题,为什么我不直接在一个函数的初始化里new这个类,然后作为整个类的属性,是不是也可以实现一整个类直接使用它?为什么一定要注入呢?其实就是结构和具体的区别。如果这么做,那整个代码就和这个具体的实现绑死了。并且如果碰上生命周期管理很短的高层类,每次请求都new一个新服务,那么若是这个服务里面有什么夸请求的数据共享,这就全没了。这种情况下最好的做法就是把服务改成单例模式——并且,别忘了前面说过的,如果真是直接new服务,那么高层和底层本质上还是耦合的,依赖倒置要求接口,但你打算在控制器里重写服务吗?接口可没办法new。

依赖注入本身是一种抽象。它对结构负责,被注入对象假设注入对象的结构,并展开逻辑,而new则是对特定的实现负责,连带结构之内的血肉也包含在内了。一个结构可以有多种具体,就像人一样,都是四肢五官,但却千奇百怪,但如果我说只能是某个具体的人,这就一下子缩限了。

高阶函数就是一种函数级别的DI,不关注具体实现,只关注函数本身,但是由于拍什么动态性质函数的结构体有时是未定义的,导致处理起来需要严格声明函数式什么样,但没办法严格在编译时规定。而python这样的动态语言 只能靠写约定,而没法在编译层面定死结构。

一般情况下,依赖注入都需要搭配控制反转