一篇文章搞懂DIP依赖倒置原则

8 阅读7分钟

一篇文章搞懂DIP依赖倒置原则

第一章:DIP 定义与核心概念

1.1 原始定义

DIP (依赖倒置原则):高层模块不应依赖低层模块,两者都应依赖抽象。抽象不应依赖细节,细节应依赖抽象,这降低了模块间的耦合度。依赖抽象使得系统更灵活,可以轻松替换具体实现而不影响上层逻辑。

1.2 什么是"倒置"?

依赖倒置不是"倒过来"或"反过来"的意思,而是把依赖方向从"高层依赖底层",变成**"高层/低层 → 抽象"**,核心是"抽象做中间人"。

  • 原本是"高层找低层要东西"
  • 现在是"高层找抽象要东西,低层按抽象标准提供东西"

依赖的"主动权"从低层转移到了抽象,这就是"倒置"。

1.3 抽象的本质

抽象是"规则/约定/接口",不是具体的东西:

  • 比如"能做咖啡"是抽象,"家用咖啡机"是具体细节
  • 抽象不关心"怎么做",只关心"要能做"

1.4 依赖方向对比

传统模式(正置):

┌─────────────────┐
│   高层模块       │
│  (Service)      │
└────────┬────────┘
         │ 依赖
         ▼
┌─────────────────┐
│   低层模块       │
│  (Repository)   │
└─────────────────┘

DIP 模式(倒置):

┌─────────────────┐
│   高层模块       │
│  (Service)      │
└────────┬────────┘
         │ 依赖
         ▼
┌─────────────────┐
│     抽象接口     │  ← 两者都依赖这个抽象
│  (Interface)    │
└────────┬────────┘
         │ 实现
         ▼
┌─────────────────┐
│   低层模块       │
│  (Repository)   │
└─────────────────┘

第二章:生活中的示例

2.1 正置例子(违反 DIP)

你(高层模块)想喝咖啡,直接依赖"家里的咖啡机"(低层模块):

  • 你会操作"这台具体的咖啡机"的按钮、加水量、放咖啡粉
  • 如果这台咖啡机坏了/换成了胶囊咖啡机,你必须重新学操作方法
  • 高层被迫适配低层的变化

2.2 倒置例子(遵循 DIP)

  1. 先定"抽象规则":不管什么咖啡机,都要遵守"能做出咖啡"的规则(抽象层:CoffeeMaker 接口)
  2. 低层适配抽象:家用咖啡机、胶囊咖啡机都按这个规则设计(比如都有"启动制作"的操作)
  3. 高层依赖抽象:你只需要会用"符合 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 的核心要点

  1. "倒置"的核心:依赖方向的倒置——从"高层直接依赖低层",变成"高低层都依赖抽象"
  2. 目的:解耦——低层的变化不会影响高层,因为高层只认抽象规则
  3. 落地关键:先定义抽象接口/规则,再让具体实现适配抽象,最后高层依赖抽象而非具体实现

6.2 一句话总结

"针对接口编程,不针对实现编程" —— 这是 DIP 的精髓。

6.3 DIP 的驱动力

驱动力说明
低层会变需要抽象隔离变化
高层复用多个高层共享同一个抽象能力

两种情况都需要考虑 DIP,核心是变化复用