一篇文章搞懂DIP依赖倒置原则
第一章:DIP 定义与核心概念
1.1 原始定义
DIP (依赖倒置原则):高层模块不应依赖低层模块,两者都应依赖抽象。抽象不应依赖细节,细节应依赖抽象,这降低了模块间的耦合度。依赖抽象使得系统更灵活,可以轻松替换具体实现而不影响上层逻辑。
1.2 什么是"倒置"?
依赖倒置不是"倒过来"或"反过来"的意思,而是把依赖方向从"高层依赖底层",变成**"高层/低层 → 抽象"**,核心是"抽象做中间人"。
- 原本是"高层找低层要东西"
- 现在是"高层找抽象要东西,低层按抽象标准提供东西"
依赖的"主动权"从低层转移到了抽象,这就是"倒置"。
1.3 抽象的本质
抽象是"规则/约定/接口",不是具体的东西:
- 比如"能做咖啡"是抽象,"家用咖啡机"是具体细节
- 抽象不关心"怎么做",只关心"要能做"
1.4 依赖方向对比
传统模式(正置):
┌─────────────────┐
│ 高层模块 │
│ (Service) │
└────────┬────────┘
│ 依赖
▼
┌─────────────────┐
│ 低层模块 │
│ (Repository) │
└─────────────────┘
DIP 模式(倒置):
┌─────────────────┐
│ 高层模块 │
│ (Service) │
└────────┬────────┘
│ 依赖
▼
┌─────────────────┐
│ 抽象接口 │ ← 两者都依赖这个抽象
│ (Interface) │
└────────┬────────┘
│ 实现
▼
┌─────────────────┐
│ 低层模块 │
│ (Repository) │
└─────────────────┘
第二章:生活中的示例
2.1 正置例子(违反 DIP)
你(高层模块)想喝咖啡,直接依赖"家里的咖啡机"(低层模块):
- 你会操作"这台具体的咖啡机"的按钮、加水量、放咖啡粉
- 如果这台咖啡机坏了/换成了胶囊咖啡机,你必须重新学操作方法
- 高层被迫适配低层的变化
2.2 倒置例子(遵循 DIP)
- 先定"抽象规则":不管什么咖啡机,都要遵守"能做出咖啡"的规则(抽象层:
CoffeeMaker接口) - 低层适配抽象:家用咖啡机、胶囊咖啡机都按这个规则设计(比如都有"启动制作"的操作)
- 高层依赖抽象:你只需要会用"符合 CoffeeMaker 规则的机器",不用管是哪一种
结果:
- 你(高层)不再依赖具体咖啡机,而是依赖"能做咖啡"这个抽象
- 咖啡机(低层)也不再是"随便设计",而是依赖"能做咖啡"的抽象规则
2.3 控制权倒置
- 传统模式:低层定义接口(方法名、参数),高层被迫适应低层
- DIP 模式:高层定义接口(我需要什么能力),低层来适应高层
谁定义接口,谁就有控制权——这就是"控制权倒置"。
第三章:编程示例(面向对象)
3.1 第一步:定义抽象(规则)
所有咖啡机都要能做咖啡(抽象不依赖细节):
interface CoffeeMaker {
makeCoffee(): void; // 抽象规则:只要是咖啡机,就得有这个方法
}
3.2 第二步:低层模块依赖抽象(细节适配规则)
class HomeCoffeeMachine implements CoffeeMaker {
makeCoffee() {
console.log("用家用咖啡机做美式咖啡");
}
}
class CapsuleCoffeeMachine implements CoffeeMaker {
makeCoffee() {
console.log("用胶囊咖啡机做拿铁");
}
}
3.3 第三步:高层模块依赖抽象(不再依赖具体低层)
class You {
// 注入抽象类型,而非具体类型(核心倒置点)
constructor(private machine: CoffeeMaker) {}
drinkCoffee() {
this.machine.makeCoffee(); // 只调用抽象规则的方法,不管具体机器
}
}
// 使用家用咖啡机
const you1 = new You(new HomeCoffeeMachine());
you1.drinkCoffee(); // 输出:用家用咖啡机做美式咖啡
// 换成胶囊咖啡机——高层(You)完全不用改,只换低层实现
const you2 = new You(new CapsuleCoffeeMachine());
you2.drinkCoffee(); // 输出:用胶囊咖啡机做拿铁
3.4 代码对比
❌ 违反 DIP 的写法:
// 高层模块直接依赖低层模块的具体实现
public class OrderService {
private MySQLOrderRepository repository = new MySQLOrderRepository();
public void createOrder(Order order) {
repository.save(order);
}
}
问题:如果要从 MySQL 切换到 Oracle,必须修改 OrderService 代码。
✅ 遵循 DIP 的写法:
// 1. 定义抽象接口
public interface OrderRepository {
void save(Order order);
}
// 2. 高层模块依赖抽象
public class OrderService {
private OrderRepository repository; // 依赖接口,不是具体实现
public OrderService(OrderRepository repository) {
this.repository = repository;
}
public void createOrder(Order order) {
repository.save(order);
}
}
// 3. 低层模块实现抽象
public class MySQLOrderRepository implements OrderRepository {
@Override
public void save(Order order) {
// MySQL 具体实现
}
}
第四章:函数式编程中的 DIP
函数式编程同样遵循 DIP,只是实现方式不同:
- 面向对象(OOP):用
接口/抽象类定义规则,用子类实现细节,用继承复用流程 - 函数式(FP):用
类型接口定义规则,用纯函数实现细节,用组合函数复用流程,用依赖注入解耦依赖
4.1 第一步:定义类型接口(抽象规则)
// types/coffeeMachine.ts
// 1. 定义差异化操作的类型(替代 OOP 的 CoffeeMachineOperations 接口)
export type CoffeeMachineOperations = {
powerOn: () => void; // 开机
putRawMaterial: () => void;// 放原料
placeCup: () => void; // 放杯子
brewCoffee: () => void; // 制作咖啡
powerOff: () => void; // 关机
};
// 2. 定义咖啡机核心方法类型(包含通用流程)
export type CoffeeMachine = {
makeCoffee: () => void; // 制作咖啡的通用流程
} & CoffeeMachineOperations; // 同时包含所有差异化操作
4.2 第二步:封装通用流程(组合函数,替代 OOP 的模板方法)
用纯函数封装"开机→放杯→放原料→制作→关机"的通用流程:
// hooks/useCoffeeMachine.ts
import { CoffeeMachine, CoffeeMachineOperations } from '@/types/coffeeMachine';
/**
* 通用咖啡机流程封装(函数式的"模板方法")
* @param operations 差异化操作实现(依赖注入,符合DIP)
* @returns 完整的咖啡机方法(通用流程+差异化操作)
*/
export function useCoffeeMachine(operations: CoffeeMachineOperations): CoffeeMachine {
// 通用流程:固定步骤顺序(复用不变的逻辑)
const makeCoffee = () => {
console.log("咖啡师开始制作咖啡:");
operations.powerOn();
operations.placeCup();
operations.putRawMaterial();
operations.brewCoffee();
operations.powerOff();
console.log("咖啡制作完成!\n");
};
// 组合:通用流程 + 差异化操作 → 完整的咖啡机
return {
...operations, // 透传所有差异化操作
makeCoffee // 挂载通用流程方法
};
}
4.3 第三步:实现具体品牌的咖啡机(纯函数)
// hooks/useBrandCoffeeMachines.ts
import { CoffeeMachineOperations } from '@/types/coffeeMachine';
import { useCoffeeMachine } from './useCoffeeMachine';
/**
* 品牌A咖啡机(纯函数实现差异化操作)
*/
export function useBrandACoffeeMachine() {
const operations: CoffeeMachineOperations = {
powerOn: () => console.log("品牌A咖啡机:按下红色电源键开机"),
placeCup: () => console.log("品牌A咖啡机:将杯子放置在右侧出水口下方"),
putRawMaterial: () => console.log("品牌A咖啡机:倒入阿拉比卡咖啡豆到左侧豆仓"),
brewCoffee: () => console.log("品牌A咖啡机:按下美式咖啡按钮,萃取30秒"),
powerOff: () => console.log("品牌A咖啡机:长按红色电源键3秒关机")
};
return useCoffeeMachine(operations);
}
/**
* 品牌B胶囊咖啡机(纯函数实现差异化操作)
*/
export function useBrandBCapsuleMachine() {
const operations: CoffeeMachineOperations = {
powerOn: () => console.log("品牌B胶囊机:点击蓝色触摸电源键开机"),
placeCup: () => console.log("品牌B胶囊机:将杯子放置在中间加热托盘上"),
putRawMaterial: () => console.log("品牌B胶囊机:将拿铁咖啡胶囊放入胶囊槽并关闭"),
brewCoffee: () => console.log("品牌B胶囊机:按下启动按钮,穿刺胶囊并萃取20秒"),
powerOff: () => console.log("品牌B胶囊机:制作完成后自动关机")
};
return useCoffeeMachine(operations);
}
第五章:DIP 的应用场景
5.1 场景一:低层会变(需要抽象隔离变化)
问题:支付方式变更
微信支付类暴露的方法:wechatPay()
支付宝支付类暴露的方法:alipay()
方法名不一致,切换支付方式时高层模块需要修改代码。
解决:定义抽象接口
┌─────────────┐
│ 订单服务 │
└──────┬──────┘
│ 依赖
▼
┌─────────────────┐
│ PaymentService │ ← 抽象层:定义 pay() 方法
│ - pay() │
└────────┬────────┘
│ 实现
┌────┴────┐
▼ ▼
┌───────┐ ┌───────┐
│ 微信 │ │ 支付宝│
│ 支付 │ │ 支付 │
└───────┘ └───────┘
5.2 场景二:高层复用(多个高层共享同一抽象能力)
多个高层模块都需要"支付能力":
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 订单服务 │ │ 充值服务 │ │ 会员服务 │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└──────────────────┼──────────────────┘
│
▼
┌─────────────┐
│ 支付接口 │ ← 抽象层
└──────┬──────┘
│
▼
┌─────────────┐
│ 微信支付 │ ← 具体实现
└─────────────┘
5.3 什么时候不需要 DIP
DIP 虽然好,但会增加代码复杂度(多了一层接口)。以下情况可以不考虑 DIP:
| 情况 | 说明 |
|---|---|
| 高层与低层是 1对1 关系 | 没有多处复用的需求 |
| 低层模块不会变化 | 不需要隔离变化 |
| 简单的工具类 | 过度设计的成本高于收益 |
第六章:总结
6.1 DIP 的核心要点
- "倒置"的核心:依赖方向的倒置——从"高层直接依赖低层",变成"高低层都依赖抽象"
- 目的:解耦——低层的变化不会影响高层,因为高层只认抽象规则
- 落地关键:先定义抽象接口/规则,再让具体实现适配抽象,最后高层依赖抽象而非具体实现
6.2 一句话总结
"针对接口编程,不针对实现编程" —— 这是 DIP 的精髓。
6.3 DIP 的驱动力
| 驱动力 | 说明 |
|---|---|
| 低层会变 | 需要抽象隔离变化 |
| 高层复用 | 多个高层共享同一个抽象能力 |
两种情况都需要考虑 DIP,核心是变化和复用。