依赖接口——组件依赖的是接口而非实现

依赖注入解决的是「同一实例怎么共享」,依赖接口解决的是「组件依赖谁」。这两个问题经常被混为一谈,但它们是完全不同的维度:一个管传递,一个管依赖方向。依赖接口是独立于依赖注入的另一条线。


一、两个问题,两个维度

1
2
3
4
5
问题 1:组件 A 需要用组件 B,B 的实例从哪来?
→ 这是依赖注入要解决的(传递维度)

问题 2:组件 A 需要用组件 B,A 依赖的是 B 的接口还是 B 的实现?
→ 这是依赖接口要解决的(依赖维度)

两个问题可以独立变化:

1
2
3
4
5
6
7
8
9
10
11
场景 1:依赖实现 + 构造函数传参
→ A 直接用 B 的具体类,通过构造函数传入
→ 依赖了实现,但用了注入

场景 2:依赖接口 + 全局变量
→ A 用 IB 接口,通过全局变量获取
→ 依赖了接口,但没用注入

场景 3:依赖接口 + 构造函数传参
→ A 用 IB 接口,通过构造函数传入
→ 既依赖接口,又用了注入

依赖接口和依赖注入是两个独立的维度,可以任意组合。


二、依赖实现 vs 依赖接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 依赖实现:A 直接用 B 的具体类
class AuthService {
PosixFileOps file_ops_; // 直接依赖 PosixFileOps
public:
void save() { file_ops_.write(data); }
};

// 依赖接口:A 用 IB 接口,不知道具体实现
class AuthService {
IFileOps& file_ops_; // 依赖 IFileOps 接口
public:
AuthService(IFileOps& ops) : file_ops_(ops) {}
void save() { file_ops_.write(data); }
};

两者的区别:

1
2
3
4
5
6
7
8
9
依赖实现:
→ AuthService 知道 file_ops 是 PosixFileOps
→ 换成 Win32FileOps 要改 AuthService 代码
→ 编译时绑定

依赖接口:
→ AuthService 不知道 file_ops 是什么具体类
→ 换成任何实现了 IFileOps 的类都不用改 AuthService
→ 运行时绑定

依赖接口的核心:组件只知道自己依赖什么能力,不知道谁提供这个能力。


三、依赖接口的三种层次

1
2
3
4
5
6
7
8
9
10
11
12
层次 1:依赖具体实现(无接口)
→ PosixFileOps 直接写在 AuthService 里
→ 最简单,但最死板

层次 2:依赖接口(有接口)
→ AuthService 依赖 IFileOps 接口
→ 可以替换实现

层次 3:依赖抽象接口 + 注入(接口 + 注入)
→ AuthService 依赖 IFileOps 接口
→ 实例通过注入传入
→ 可替换 + 可共享

四、依赖接口的代价

依赖接口不是免费的:

1
2
3
4
5
6
7
8
9
10
11
代价 1:间接调用
→ 通过接口调用比直接调用慢一点(虚函数开销或函数指针开销)

代价 2:设计复杂度
→ 需要定义接口类、管理继承关系

代价 3:过度设计风险
→ 如果实现确定不会替换,依赖接口就是浪费

代价 4:调试困难
→ 通过接口调用时,调试器看到的是接口类型,不是具体类型

不是所有地方都需要依赖接口。 只有在「实现可能替换」时才需要。


五、什么时候依赖接口

1
2
3
4
5
6
7
8
9
10
需要依赖接口的场景:
→ 测试时需要 Mock(模拟实现)
→ 多平台需要替换实现(POSIX / Win32)
→ 插件系统需要动态加载
→ 子系统之间需要解耦

不需要依赖接口的场景:
→ 实现确定不会替换
→ 性能是关键瓶颈(间接调用开销不可接受)
→ 简单工具函数(本来就是无状态的函数导出)

六、依赖接口和依赖注入的关系

1
2
3
4
5
6
7
依赖接口 = 依赖什么(What)
依赖注入 = 怎么拿到(How)

两者可以独立使用:
依赖接口 + 不注入 → 接口固定,实例手动创建
不依赖接口 + 注入 → 实现固定,实例通过注入共享
依赖接口 + 注入 → 接口可替换 + 实例可共享 = 完整解耦

依赖接口解决依赖方向,依赖注入解决实例传递。两者是正交的维度。


七、总结

1
2
3
4
5
6
7
8
9
10
11
依赖接口:

依赖实现 → 编译时绑定,实现固定
依赖接口 → 运行时绑定,实现可替换

选择依据:实现会不会替换?

和依赖注入的关系:
依赖接口 = 依赖什么(What)
依赖注入 = 怎么拿到(How)
两者正交,可以独立使用