【控制反转】和【依赖注入】基本上是每个java程序员入门必学的概念
【鸡蛋灌饼】也是每个打工人必(然)吃(过)的口粮
那么它们之间有什么关系呢?
今天想用这篇文章聊聊如何用鸡蛋灌饼更直观地理解控制反转IoC和依赖注入DI的核心思想
1.首先我们先了解什么是控制反转和依赖注入
控制反转(Inversion of Control)
是一种设计原则,其核心是将对象的创建、生命周期管理及依赖关系的控制从代码中剥离,交由外部容器或框架统一管理。传统开发中,对象通过直接实例化(如new一个)主动获取依赖,而反转则是将控制权交给容器,使对象仅需声明依赖,由容器在运行时动态注入
核心特征:
控制权转移:从代码内部转移至容器
解耦与扩展性:组件间依赖关系不再硬编码,而是通过配置或注解动态解析
依赖注入(Dependency Injection)
是实现控制反转的具体手段,将依赖对象从外部注入到目标组件汇总,而非由组件自行创建。
3种实现方式:
构造函数注入:通过构造参数传递依赖项
Setter方法注入:通过公共方法动态设置依赖项
接口/属性注入:直接通过接口或属性赋值注入
两者均致力于降低组件耦合度,提高代码可维护性和可测试性,实现灵活扩展
松耦合:组件仅依赖接口或抽象,而非具体实现
可维护性:依赖关系集中管理,修改时无需调整业务代码
可测试性:依赖项可灵活替换为模拟对象,便于单元测试
2.用鸡蛋灌饼来理解控制反转IoC和依赖注入DI的核心思想
1. 传统开发模式(无IoC/DI)
场景:
你作为程序员想吃鸡蛋灌饼,需要 自己动手做:
- 买面粉、鸡蛋、生菜 → 自己创建所有依赖对象
- 和面、煎饼、调酱料 → 内部实现所有细节
- 吃完还要洗碗 → 管理资源生命周期
代码类比:
// 你完全控制所有过程,耦合度高
public class Programmer {
public void eat() { // 自己造轮子
Dough dough = new Dough(); // 自己买面粉和面
Egg egg = new Egg(); // 自己买鸡蛋
Pancake pancake = dough.fry().add(egg); // 自己煎饼
pancake.eat(); //吃
pancake.clean(); // 自己洗碗
}
}
2. 控制反转(IoC)
场景:
你选择 去摊位购买,而非自己做:
- 摊主大妈(容器)掌控流程:和面、煎饼、加料 → 控制权反转到了大妈手里
- 你只需说需求:“加两个蛋或者肠,微辣” → 定义接口(抽象)
- 摊主做好递给你 → 返回实例
代码类比:
// 你依赖外部容器(摊主)提供服务
public class Programmer {
private PancakeSeller seller; // 依赖抽象(摊位接口)
// 依赖注入:通过构造函数传入具体摊主
public Programmer(PancakeSeller seller) {
this.seller = seller;
}
public void eat() { // 无需关心实现细节,直接调用接口
Pancake pancake = seller.makePancake("双蛋微辣"); // 控制权在摊主
pancake.eat(); // 不用洗碗!生命周期由摊主管理
}
}
3. 依赖注入(DI)的3种实现方式
(1) 构造函数注入(最常见)
场景:
你在王阿姨摊位前说:“我要一个鸡蛋灌饼,加双蛋” → 声明依赖关系
王阿姨(具体实现类)将灌饼做好递给你 → 通过构造函数注入依赖
代码类比:
PancakeSeller seller = new WangAyiPancake(); // 具体实现:王阿姨摊位
Programmer you = new Programmer(seller); // 依赖注入
you.eat();
(2) Setter方法注入
场景:
你原本买了原味灌饼,中途说:“等等,加一勺辣酱!” → 动态修改依赖
代码类比:
public class Programmer {
private PancakeSeller seller; // Setter方法注入
public void setSeller(PancakeSeller seller) {
this.seller = seller;
}
}
Programmer you = new Programmer();
you.setSeller(new SpicyPancakeSeller()); // 动态加了辣酱
you.eat();
(3) 接口注入(较少用)
场景:
摊位提供“定制调料”服务,你必须实现一个“加料接口”才能用 → 通过接口强制注入
代码世界少用,现实世界也还没实现呢,这世界何尝不是一个巨大的hello world
代码类比:
public interface SauceService {
void injectSauce(Sauce sauce);
}
public class Programmer implements SauceService {
private Sauce sauce;
@Override
public void injectSauce(Sauce sauce){
this.sauce = sauce; // 实现接口完成注入
}
}
Programmer you = new Programmer();
you.injectSauce(new ChiliSauce()); // 注入辣酱
4. 为什么IoC/DI更好?
-
解耦:
- 你不用关心摊主用哪家面粉、鸡蛋品牌 → 依赖抽象,不绑定具体实现
- 换摊主(如从“王阿姨”换到“李大爷”)只需修改注入对象 → 更换实现类不影响主逻辑
-
可测试性:
- 单元测试时,用
MockPancakeSeller
模拟摊主 → 避免依赖真实摊位
- 单元测试时,用
-
可维护性:
做鸡蛋灌饼的流程已经确定了,至于她每天用哪家的鸡蛋或者生菜,影响不大,你能吃出来吗?
-
扩展性:
- 新增“芝士灌饼”只需实现
PancakeSeller
接口 → 开闭原则(对扩展开放)
- 新增“芝士灌饼”只需实现
总结:买灌饼 vs IoC/DI
动作 | 买灌饼 | IoC/DI |
---|---|---|
核心需求 | 吃灌饼 | 获取对象实例 |
实现者 | 摊主大妈(容器) | IoC容器(如Spring) |
控制权 | 摊主控制流程 | 容器管理对象生命周期 |
依赖关系 | 你依赖摊主,但不关心(不控制)Ta怎么实现 | 类依赖接口,而非具体实现类 |
灵活性 | 换摊主只需走到另一家 | 更换实现类只需修改注入配置 |
核心思想:
- 程序员(你) 应专注业务逻辑(吃灌饼),而非资源管理(和面、煎饼)。
- 摊主(容器) 负责依赖创建和调度,实现控制权反转。
注:一时心血来潮,既然面向对象是对现实世界的理解和抽象,那么它们之间肯定是可以相互关联的,或者每一种思想或机制都可以在现实世界找到类似的注解,所以先用最基础的两个概念给这一系列栏目开个篇,名字还没想好,暂时叫【深入浅出之编程概念】吧,哈哈哈^_^
如有不合适地方,欢迎各位程序员指正或者友好讨论