一、依赖注入基础:什么是依赖注入、控制反转(IoC)概念、为什么需要DI、DI的优缺点分析
1.1 从一个让我头疼的项目说起
我记得刚入行那会儿,接手过一个老项目。
那个项目里,业务逻辑层直接 new 数据访问层。看起来没什么问题,对吧?
但后来客户说:「数据库要从 SQL Server 换成 Oracle。」
好家伙,整个团队忙活了两个月。因为每一处 new SqlConnection 的地方都得改。改完还得重新测试,生怕漏掉哪个角落。
你想想看,这还只是换数据库。要是换日志组件、换缓存方案、换消息队列呢?
说白了,这种硬编码依赖的方式,让系统变得极其脆弱。每次改动都像拆炸弹。
这就是我们今天要聊的——依赖注入(DI)要解决的核心问题。
1.2 什么是依赖注入?
依赖注入,英文叫 Dependency Injection,简称 DI。
名字听着挺唬人,其实道理很简单:
一个类需要的依赖,不要自己创建,让别人给你送过来。
举个例子。假设你开了一家餐厅:
- 没有 DI 的做法:厨师自己种菜、自己养猪、自己磨面粉。什么都自己干,累死不说,换个供应商得把厨房拆了重建。
- 有 DI 的做法:厨师只管炒菜。菜有人送,肉有人供,面粉有人管。厨师只需要说「我要什么」,自然有人送上门。
在代码里,就是这样的区别:
// 没有 DI —— 类自己创建依赖
public class OrderService
{
private readonly ILogger _logger;
public OrderService()
{
// 自己 new 出来
_logger = new FileLogger();
}
}
// 有 DI —— 依赖从外部注入
public class OrderService
{
private readonly ILogger _logger;
public OrderService(ILogger logger)
{
// 别人给我,我只管用
_logger = logger;
}
}
看到区别了吗?第二种写法里,OrderService 不再关心 logger 是写文件、写数据库还是写控制台。它只管调用。
核心要点:依赖注入的本质,就是把「创建依赖」这件事,从类的内部移到外部。类只负责「使用」,不负责「创建」。
1.3 控制反转(IoC)—— 到底反转了什么?
聊 DI 就绕不开 IoC(控制反转)。这两个概念经常被混用,我简单说说我的理解。
控制反转,反转的是「控制权」。
传统写法里,控制权在类自己手里——我自己 new 依赖,自己决定用哪个实现。
用了 IoC 之后,控制权交给了外部容器。类不再主动创建依赖,而是被动等待注入。
打个比方:
- 传统方式:你去饭店,自己进厨房炒菜。控制权在你手里。
- IoC 方式:你坐下点菜,厨师做好端上来。控制权在厨师(容器)手里。
所以 IoC 是一种设计思想,DI 是实现这种思想的具体技术手段。
我的习惯:我一般把 IoC 看作「原则」,把 DI 看作「实践」。面试时如果有人问区别,我会说:IoC 是「我要什么你给我什么」,DI 是「你怎么给我的具体方式」。
1.4 为什么需要依赖注入?
这个问题,我用三个字回答:解耦合。
但光说解耦合太抽象了。我具体说说我在项目中遇到的几个场景:
场景一:单元测试
没有 DI 的代码,测试起来特别痛苦。因为你要测 OrderService,就得连数据库、连文件系统、连网络。准备工作比测试本身还长。
有了 DI,我可以注入一个 Mock 的 ILogger,注入一个 Mock 的 IRepository。测试变得干净利落。
场景二:替换实现
我曾经有个项目,日志一开始用 log4net,后来要换成 Serilog。
没有 DI 的话,得满世界找 new log4net.Logger() 的地方。有 DI 的话,改一行注册代码就行。
场景三:生命周期管理
有些对象我们希望整个请求共用同一个实例(比如 DbContext),有些希望每次都用新的(比如轻量级工具类)。
DI 容器天然支持这些生命周期管理。你不用自己写单例模式,不用自己搞线程安全。
一句话总结:DI 让代码更灵活、更可测、更易维护。说白了,就是让你改代码的时候少掉头发。
1.5 依赖注入的优缺点分析
任何技术都有两面性。DI 也不是银弹。我客观说说它的优缺点。
优点
| 优点 | 说明 |
|---|---|
| 降低耦合度 | 类之间通过接口交互,不依赖具体实现 |
| 提高可测试性 | 可以轻松注入 Mock 对象进行单元测试 |
| 增强可维护性 | 替换实现只需改注册代码,不用改业务逻辑 |
| 生命周期管理 | 容器自动管理对象的创建和销毁 |
| 促进面向接口编程 | 强制开发者思考抽象,而不是具体实现 |
缺点
| 缺点 | 说明 |
|---|---|
| 学习曲线 | 新手理解 IoC 和 DI 需要时间 |
| 调试困难 | 运行时才知道具体注入的是哪个实现,堆栈跟踪变复杂 |
| 性能开销 | 反射和动态代理会带来轻微性能损失(通常可忽略) |
| 过度设计风险 | 小项目硬上 DI,反而增加复杂度 |
| 配置复杂 | 依赖关系复杂时,注册代码可能变得冗长 |
避坑指南:我曾经在一个只有三个控制台程序的项目里强行上了 DI 全套。结果代码量翻了一倍,维护起来反而更麻烦。后来我学乖了——DI 是好东西,但别滥用。小项目、原型阶段,直接 new 反而更高效。
1.6 什么时候该用,什么时候不该用?
我个人的经验是:
- 该用:项目规模中等以上、需要单元测试、可能替换实现、多人协作
- 慎用:简单工具类、一次性脚本、性能极端敏感的场景、原型验证阶段
嗯,这里要注意一点:DI 不是非黑即白的。你可以只在需要的地方用 DI,其他地方保持简单。别为了用 DI 而用 DI。
1.7 小结
这一章我们聊了:
- 依赖注入是什么——让别人把依赖送过来
- 控制反转是什么——控制权从类内部转移到外部容器
- 为什么需要 DI——解耦合、好测试、易维护
- DI 的优缺点——灵活但有代价,别滥用
下一章,我会带你看看 .NET 里 DI 容器的具体用法。包括怎么注册、怎么解析、三种生命周期有什么区别。到时候我会拿我踩过的坑当反面教材,保证让你印象深刻。
课后思考:你现在写的代码里,有没有那种「new 来 new 去」的地方?试着想想,如果改成依赖注入,会带来什么好处?又会引入什么麻烦?