上一篇介绍了 SOLID 的前三个字母 SRP・OCP・LSP,今天来整理剩下的两个字母。
它们是 ISP(接口隔离原则)和 DIP(依赖倒置原则)。
尤其是 DIP,它是使用 Swinject 或 Factory 等 Swift DI 库时必然会遇到的“依赖注入(DI)”理论基础。读完这一篇,你就会明白这些库为什么会设计成现在这样。
ISP:接口隔离原则
先从定义开始。
不应强迫客户端依赖自己不使用的方法。
这句话听起来有些抽象,但看一个实际问题就能马上理解。假设我们创建了一个多功能打印机协议。
protocol Machine {
func print(_ doc: Document)
func scan(_ doc: Document)
func fax(_ doc: Document)
}
// 旧式打印机只能打印...
class OldPrinter: Machine {
func print(_ doc: Document) { /* 正常工作 */ }
func scan(_ doc: Document) { fatalError("无法扫描") } // 强行实现
func fax(_ doc: Document) { fatalError("无法传真") } // 强行实现
}
这相当于强迫一台只能打印的打印机实现扫描和传真。最终会出现只抛出异常的伪方法,而这也违反了上一篇介绍的 LSP。臃肿的接口就这样连续破坏了两个原则。
解决方法很简单:按职责拆分接口。
protocol Printer { func print(_ doc: Document) }
protocol Scanner { func scan(_ doc: Document) }
protocol Fax { func fax(_ doc: Document) }
class OldPrinter: Printer { ... } // 仅打印
class MultiFunction: Printer, Scanner, Fax { ... } // 全功能多功能打印机
各自只承诺自己能做到的事情。在实际开发中,通常会出现这样的信号:实现协议时产生了“这个方法和我们没关系……”的空实现,那就说明该拆分协议了。
DIP:依赖倒置原则
这是一个因为名称而经常被误解的原则。定义只有两行。
高层模块不应依赖低层模块。两者都应依赖抽象。
抽象不应依赖细节。细节应依赖抽象。
我们用代码来看。假设订单服务直接使用电子邮件发送库。
// 高层模块(业务逻辑)直接依赖低层模块(实现细节)
import SendGrid
class OrderService {
private let mailer = SendGridClient(apiKey: apiKey)
func completeOrder(_ order: Order) {
// 订单处理...
mailer.send(to: order.email, message: "订单完成") // SendGrid被绑定
}
}
这种结构有两个问题。要把 SendGrid 换成其他服务,就必须修改业务逻辑,而且每次测试都会真正发出邮件。
应用 DIP 后,箭头的方向会反转。
// 抽象由高层模块(订单领域)定义
protocol NotificationSender {
func send(to: String, message: String) async throws
}
class OrderService {
private let sender: NotificationSender
init(sender: NotificationSender) { // 只依赖抽象
self.sender = sender
}
func completeOrder(_ order: Order) async throws {
try await sender.send(to: order.email, message: "订单完成")
}
}
// 细节(SendGrid)遵循抽象
class SendGridSender: NotificationSender { ... }
class SlackSender: NotificationSender { ... }
class FakeSender: NotificationSender { ... } // 测试用
原本指向“订单服务 → SendGrid”的依赖关系,现在变成了“订单服务 → 协议 ← SendGrid”。由于细节向领域低头,因此称为“倒置”。
所以才会有 DI 框架
这里自然会产生一个问题:“那么,SendGridSender()是谁创建的?”
负责完成这次组装的,正是依赖注入(DI)容器。Swift 生态中的 Swinject、Factory 等 DI 库,准确来说做的就是这件事。让类只依赖协议,而实际实现则由库负责注入。
DIP 是原则(方向),DI 是实现它的技术(工具)。只要掌握这个区别,面试回答就能提升一个层次。
SOLID 全面总结
把两篇文章介绍的 SOLID 压缩成五行,就是这样。
| 原则 | 一句话总结 |
|---|---|
| SRP | 如果提出修改的人不同,就将代码分离 |
| OCP | 经常变化的地方,用新增而不是修改来应对 |
| LSP | 子类不能破坏父类的约定 |
| ISP | 不要强迫实现永远不会使用的方法 |
| DIP | 不要让业务逻辑依赖细节 |
五个原则最终都指向同一个方向:缩小变更的影响范围。
不过,如果把这些原则机械地应用到所有代码上,反而会违反 KISS 和 YAGNI。原则是应该选择性地用于实际经常发生变化的地方的工具。我认为,这种平衡感才是真正的能力。

![[SOLID #2] SOLID 原则实战篇(下):ISP・DIP,以及依赖注入存在的原因 封面图](/assets/images/posts/6d8f2c8e-6a69-4df6-a991-c232fcf9500a/1.jpg)