依赖接口——组件依赖的是接口而非实现
依赖注入解决的是「同一实例怎么共享」,依赖接口解决的是「组件依赖谁」。这两个问题经常被混为一谈,但它们是完全不同的维度:一个管传递,一个管依赖方向。依赖接口是独立于依赖注入的另一条线。
一、两个问题,两个维度
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
| class AuthService { PosixFileOps file_ops_; public: void save() { file_ops_.write(data); } };
class AuthService { IFileOps& file_ops_; 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) 两者正交,可以独立使用
|