面试时被问过“请解释一下 SOLID 原则”这个问题的人,应该都有过一次吧。
但说实话,即使能把五条原则逐一背出来,能继续回答“那该怎么用在我的代码里?”的人也并不多。
这次不讲教科书定义,而是通过实际工作场景来拆解 SOLID。内容较多,所以分成上下两篇。今天先讲前三个字母:SRP、OCP 和 LSP。
SOLID 是 Robert Martin(Uncle Bob)整理出的面向对象设计五大原则的首字母缩写。
- SRP — 单一职责原则
- OCP — 开闭原则
- LSP — 里氏替换原则
- ISP — 接口隔离原则
- DIP — 依赖倒置原则
五条原则看起来各自讨论不同的问题,但贯穿其中的核心信息只有一个:发生变更时,缩小需要修改的代码范围。
SRP — 单一职责原则
教科书中的定义是“一个类应该只有一个职责”,但 Uncle Bob 本人后来用更好的方式重新表述了它。
一个模块应该只对一个行为者负责。
这里的行为者,是指要求修改这段代码的人或部门。假设我们有这样一个类。
class Employee {
func calculatePay() { ... } // 会计团队要求修改
func reportHours() { ... } // 人力资源团队要求修改
func save() { ... } // 开发团队(DBA)要求修改
}
三个方法的负责人完全不同。如果因会计团队的需求修改 calculatePay时碰到了共享逻辑,人力资源报表可能会悄悄出错。实际发生这种事故时,也很难找到原因。
SRP 将其总结为:“要求修改的人不同,代码也要分开。”如果把工资计算、工作报表和存储分别拆成不同的类,会计团队的需求就不会影响人力资源功能。
OCP — 开闭原则
定义是“对扩展开放,对修改关闭”。刚听到时像是禅宗问答,但放到代码中就很清楚了。
// 如果每增加一种支付方式都要修改这个函数
func pay(method: String, amount: Int) {
if method == "card" { ... }
else if method == "kakaopay" { ... }
else if method == "naverpay" { ... }
// 新支付方式 = 修改 else if = 现有代码
}
在这种结构中,要添加 Toss Pay,就必须打开并修改原本运行正常的函数。每次修改都要承担破坏现有支付方式的风险。
// 只需“添加”新的支付方式即可的结构
let payments: [String: Payment] = [
"card": CardPayment(),
"kakaopay": KakaoPayment(),
"naverpay": NaverPayment(),
// 添加 Toss Pay = 一行注册新类
]
func pay(method: String, amount: Int) {
payments[method]?.process(amount: amount)
}
无需触碰现有代码(对修改关闭),只要添加新类就能扩展功能(对扩展开放)。诀窍是只在经常变化的地方采用这种结构。如果所有代码都这样做,就会违反上一篇提到的 YAGNI。
LSP — 里氏替换原则
名字看起来最吓人,但含义很简单。
子类应该可以直接替换父类出现在任何位置,而不会破坏程序。
最著名的反例是矩形和正方形问题。如果按照“正方形是矩形”这一数学常识来继承,就会发生这种情况。
class Rectangle {
var width = 0
var height = 0
func setWidth(_ w: Int) { width = w }
func setHeight(_ h: Int) { height = h }
}
class Square: Rectangle {
override func setWidth(_ w: Int) { width = w; height = w } // 因为它是正方形
override func setHeight(_ h: Int) { width = h; height = h }
}
// 期待矩形的代码
func test(_ rect: Rectangle) {
rect.setWidth(5)
rect.setHeight(4)
print(rect.width * rect.height) // 20期待 Square但如果是正方形 16
}
一旦传入 Square,“宽 5、高 4 时面积为 20”这一理所当然的预期就被打破了。即使 is-a 关系看似成立,只要破坏行为契约,就不应该使用继承,这就是 LSP。
实际工作中的信号是这样的:如果子类重写父类方法后抛出异常,或者留下空实现,那么这种继承很可能违反了 LSP。
上篇到此结束
把今天介绍的三条原则各留下一句话吧。
- SRP — 要求修改的人不同,代码也要分开。
- OCP — 让经常变化的地方能够通过添加而不是修改来应对。
- LSP — 子类不要破坏父类的契约。如果必须破坏,说明继承就是错误的。
下一篇将介绍剩下的两个字母:ISP(接口隔离)和 DIP(依赖倒置)。尤其是 DIP,它与现代框架为什么都使用依赖注入直接相关,应该会很有趣。

![[SOLID #1] SOLID 五个字母,不要死记而要理解(SRP・OCP・LSP) 封面图](/assets/images/posts/7b95a0fb-b586-4f62-b56e-4a8b31a29dfd/1.jpg)