5. 依赖倒置原则:面向接口编程、依赖抽象而非具体、实战:Spring IoC容器思想
依赖倒置原则,英文叫 DIP(Dependency Inversion Principle)。
名字听着挺唬人,其实核心就两句话:
- 高层模块不应该依赖低层模块,两者都应该依赖抽象。
- 抽象不应该依赖细节,细节应该依赖抽象。
说白了,就是让我们面向接口编程,依赖抽象而非具体实现。
我刚开始写 Java 那几年,对这个原则的理解也停留在“哦,就是多用接口嘛”。直到后来接手一个老项目,改一个功能要动七八个类,我才真正体会到——不遵守 DIP 的代码,改起来是真的疼。
5.1 什么是“倒置”?
传统的思维是:高层调用低层。比如 Service 直接 new 一个 Dao 对象。
但依赖倒置把这个关系“倒”过来了——高层和低层都去依赖一个抽象接口。谁都不直接依赖谁。
你想想看,这样有什么好处?
- 低层实现想换就换,高层不用改代码。
- 测试的时候可以轻松 mock 掉低层。
- 系统扩展性大大增强。
核心思想:不要依赖具体类,要依赖抽象(接口或抽象类)。
5.2 反面教材:依赖具体实现
先看一段我早期写过的代码,嗯,有点丢人,但很真实:
public class OrderService {
private MySqlOrderDao dao = new MySqlOrderDao(); // 直接依赖具体实现
public void createOrder(Order order) {
dao.save(order);
}
}
这段代码有什么问题?
- OrderService 直接绑死了 MySqlOrderDao。
- 如果哪天要换成 Oracle 或者 MongoDB,必须改 OrderService 的代码。
- 单元测试时,没法 mock 掉 dao,只能连真实数据库测。
我曾经在一个项目中见过类似的代码,数据库从 MySQL 切到 TiDB,光改这种硬编码就改了两天,还漏了几个地方没改到,线上出了故障。从那以后,我对这种写法就零容忍了。
5.3 正确姿势:依赖抽象
按照 DIP 原则,我们应该这样写:
// 1. 定义抽象接口
public interface OrderDao {
void save(Order order);
}
// 2. 具体实现
public class MySqlOrderDao implements OrderDao {
public void save(Order order) {
// MySQL 保存逻辑
}
}
// 3. 高层模块依赖抽象
public class OrderService {
private OrderDao dao; // 依赖接口,不依赖具体类
public OrderService(OrderDao dao) { // 通过构造器注入
this.dao = dao;
}
public void createOrder(Order order) {
dao.save(order);
}
}
看到区别了吗?OrderService 不再关心 dao 到底是 MySQL 还是 Oracle。它只认 OrderDao 这个接口。
换数据库?简单,写一个新的实现类,注入进去就行。OrderService 一行代码都不用改。
个人习惯:我一般会把接口定义在单独的模块或包中,和具体实现完全隔离。这样依赖关系更清晰。
5.4 实战:Spring IoC 容器思想
说到依赖倒置,就不得不提 Spring 的 IoC(控制反转)容器。
IoC 是 DIP 的一种具体实现方式。它把“谁控制谁”这个问题给反转了:
- 传统方式:对象自己控制依赖的创建(new 出来)。
- IoC 方式:对象的依赖由容器来创建和注入。
Spring 的依赖注入(DI)就是 IoC 的具体表现。我们常用的 @Autowired 注解,其实就是让 Spring 帮我们把依赖的对象注入进来。
@Service
public class OrderService {
@Autowired
private OrderDao dao; // Spring 容器注入,不是自己 new 的
public void createOrder(Order order) {
dao.save(order);
}
}
你看,OrderService 里完全没有 new 的痕迹。它只知道自己需要一个 OrderDao,具体是谁?由 Spring 在运行时决定。
注意:@Autowired 虽然方便,但建议优先使用构造器注入(Constructor Injection)。这样能保证依赖不可变,也方便单元测试。
5.5 依赖倒置的核心逻辑图
下面这张 SVG 图展示了 DIP 的核心思想:高层和低层都依赖抽象,而不是互相依赖。
5.6 避坑指南
依赖倒置虽好,但也不是银弹。我踩过几个坑,分享给你:
- 不要过度抽象。 如果一个接口只有一个实现,而且短期内不可能有第二个,那强行抽象就是过度设计。我曾经见过一个项目,连工具类都抽象出接口,结果代码量翻倍,维护成本反而更高。
- 接口粒度要适中。 太粗的接口(比如一个 Dao 接口包含所有方法)会导致实现类被迫实现不需要的方法。太细的接口又会导致接口数量爆炸。
- 依赖注入不要滥用。 不是所有地方都需要 Spring 容器来管理。简单的工具类、值对象,直接 new 就好。
我的建议:刚开始实践 DIP 时,可以从“变化点”入手。那些你觉得未来可能会换的实现,优先抽象成接口。稳定的部分,先保持简单。
5.7 小结
依赖倒置原则,说白了就是一句话:面向接口编程,依赖抽象而非具体。
它让我们的代码从“硬编码”变成“可配置”,从“紧耦合”变成“松耦合”。Spring 的 IoC 容器就是这一思想的最佳实践。
记住:写代码的时候,多问问自己——“这个依赖,我能不能抽象成接口?”
公众号:蓝海资料掘金营,微信 deep3321