为什么要学 SOLID?
在开发项目时,我们每天都在"改需求":
- 产品经理说"再加一种支付方式"
- 后端说"换一套登录接口"
- 设计师说"按钮样式统一换"
如果代码耦合严重,每一次改动都会牵一发动全身。
SOLID 五原则就是为了让"改动"变得安全、可预期、可测试,最终让"未来的自己"少掉两根头发。
SOLID 速览
缩写 | 全称 | 一句话记忆 |
---|---|---|
SRP | Single Responsibility Principle单一职责原则 | 一个类只负责一件事 |
OCP | Open-Closed Principle开闭原则 | 对扩展开放,对修改关闭 |
LSP | Liskov Substitution Principle里氏替换原则 | 子类必须能无缝替换父类 |
ISP | Interface Segregation Principle接口隔离原则 | 别让客户端依赖它不需要的接口 |
DIP | Dependency Inversion Principle依赖倒置原则 | 依赖抽象,而非具体实现 |
下面用 仓颉 代码逐一拆解
S -- Single Responsibility Principle 单一职责原则
概念:一个类只能因为"一个原因"而改变。
反例:UserManager 同时管注册、发邮件、存数据库。
cangjie
class User {}
// ❌ 违反 SRP:UserManager 负责三件事
class UserManager {
func registerUser(email: String, password: String): Bool {
// 1. 注册逻辑
return true
}
func sendWelcomeEmail(to: String, user: User): Unit {
// 2. 发邮件逻辑
}
func saveUserData(user: User): Unit {
// 3. 持久化逻辑
}
}
重构:把三件事拆成三个类,每个类只有一个"变更理由"。
cangjie
// ✅ 负责注册
class UserRegistrationService {
func register(email: String, password: String): Bool {
// 仅处理注册
return true
}
}
// ✅ 负责邮件
class EmailService {
func sendWelcomeEmail(to: String, user: User): Unit {
// 仅处理邮件
}
}
// ✅ 负责持久化
class UserRepository: Unit {
func save(user: User) {
// 仅处理数据库/磁盘
}
}
收益:
- 邮件模板要改?只动 EmailService
- 存储换成 Realm?只动 UserRepository
O -- Open-Closed Principle 开闭原则
概念:新增功能时,不修改老代码,只添加新代码。 反例:用 match 判断支付类型,每加一种支付方式都要改老文件。
cangjie
// ❌ 违反 OCP:新增支付类型必须改 PaymentProcessor
class PaymentProcessor {
func process(tp: String, amount: Float64): Unit {
match (tp) {
case "creditCard" => println("刷卡 ${amount}")
case "paypal" => println("PayPal ${amount}")
case _ => println("不支持")
}
}
}
重构:面向协议编程(POP),把"可变点"封装成协议。
cangjie
// 1. 定义不变协议
interface PaymentMethod {
func process(amount: Float64): Unit
}
// 2. 想加多少种支付就加多少类
class CreditCard <: PaymentMethod {
public func process(amount: Float64): Unit {
print("刷卡 ${amount}")
}
}
class PayPal <: PaymentMethod {
public func process(amount: Float64): Unit {
print("PayPal ${amount}")
}
}
// 3. 处理器对扩展永远关闭修改
class PaymentProcessor {
func process(method: PaymentMethod, amount: Float64): Unit {
method.process(amount) // 多态,无需 switch
}
}
// 4. 新增 Apple Pay,老文件一行不动
class ApplePay <: PaymentMethod {
public func process(amount: Float64): Unit {
print("Apple Pay ${amount}")
}
}
L -- Liskov Substitution Principle 里氏替换原则
概念:父类出现的地方,子类必须能无异常替换;子类不能"违约"。 反例:Bird 协议要求会飞,Penguin 被迫实现 fly() 却 crash。
cangjie
// ❌ 违反 LSP:Penguin 不能飞,却强行 override
open class Bird {
public open func fly(): Unit {
println("飞")
}
}
class Penguin <: Bird {
public override func fly(): Unit {
throw Exception("企鹅不会飞!") // 运行时爆炸
}
}
func makeFly(bird: Bird) {
bird.fly() // 传 Penguin 会崩溃
}
重构:把"飞"行为抽象成更小的协议,让真正的"飞行者"去遵守。
cangjie
// 1. 仅飞行者才需要实现
interface Flyable {
func fly(): Unit
}
// 2. 企鹅不会飞,就不实现 Flyable
class Eagle <: Flyable {
public func fly(): Unit {
println("鹰击长空")
}
}
class Penguin {
public func swim(): Unit {
println("企鹅游泳")
}
}
// 3. 高阶函数只接受 Flyable,类型安全
func makeFly(f: Flyable): Unit {
f.fly()
}
I -- Interface Segregation Principle 接口隔离原则
概念:客户端不应被迫依赖它用不到的接口。 反例:Worker 协议同时要求 work() 和 eat(),机器人被迫空实现 eat()。
cangjie
// ❌ 违反 ISP:Robot 不需要 eat,却必须"假实现"
interface Worker {
func work(): Unit
func eat(): Unit
}
class HumanWorker <: Worker {
public func work(): Unit {
print("人工 work")
}
public func eat(): Unit {
print("人工 eat")
}
}
class RobotWorker <: Worker {
public func work(): Unit {
print("机器人 work")
}
public func eat(): Unit { /* 机器人不吃,空实现 */ }
}
重构:拆成"小接口",按需组合。
cangjie
// 1. 行为细分
interface Workable {
func work(): Unit
}
interface Eatable {
func eat(): Unit
}
// 2. 人类两个都要
class HumanWorker <: Workable & Eatable {
public func work(): Unit {
print("人工 work")
}
public func eat(): Unit {
print("人工 eat")
}
}
// 3. 机器人只实现 Workable
class RobotWorker <: Workable {
public func work(): Unit {
print("机器人 work")
}
}
D -- Dependency Inversion Principle 依赖倒置原则
概念:
- 高层不依赖低层,二者都依赖抽象。
- 抽象不依赖细节,细节依赖抽象。
反例:DataManager 直接初始化 LowLevelStorage,换数据库要改高层。
cangjie
// ❌ 违反 DIP:高层依赖具体实现
class LowLevelStorage {
func store(data: String): Unit {
println("存磁盘 ${data}")
}
}
class DataManager {
private let storage = LowLevelStorage() // 硬编码具体类
func save(data: String): Unit {
storage.store(data)
}
}
重构:通过协议 + 依赖注入(DI)倒转依赖方向。
cangjie
// 1. 抽象协议
interface Storage {
func store(data: String): Unit
}
// 2. 具体实现
class DiskStorage <: Storage {
public func store(data: String): Unit {
println("存磁盘 ${data}")
}
}
class CloudStorage <: Storage {
public func store(data: String): Unit {
println("存云端 ${data}")
}
}
// 3. 高层只依赖抽象
class DataManager {
DataManager(private let storage!: Storage) {}
func save(data: String): Unit {
storage.store(data)
}
}
func demo(): Unit {
// 4. 使用时自由切换
let manager = DataManager(storage: CloudStorage())
manager.save("Hello DIP")
}
实战总结与踩坑
-
SRP 最难的是"粒度"
拆太细 → 类爆炸;拆太粗 → UserManager类。
判断标准:如果一段逻辑因为两个完全不同的需求而变更,就拆分。
-
OCP 在 cangjie 最自然的工具是协议 + 泛型 + 扩展。
可以在不修改原有方法实现的情况下新增实现
-
LSP 常被忽略,尤其在"继承狂热"场景。
建议:优先用组合+协议,少用继承;如果必须继承,子类只能加强、不能削弱父类行为(返回值更具体、异常更少)。
-
ISP可以与仓颉的协议继承一起使用
例如
public interface Equatable<T> <: Equal<T> & NotEqual<T>
就是 ISP 思想。 -
DIP 是架构级别的"依赖倒转器"。
结语
SOLID 不是教条,而是"让代码拥抱变化"的底层思维。
当你习惯把"可能变化"的方向抽象成协议、把"单一理由"拆成独立模块、把"继承"换成"组合",你会发现:
- 单元测试更好写(Mock 协议即可)
- Code Review 更少冲突(改动隔离)
- 新人上手更快(类名即职责)
下一次需求变更来临,你可以优雅地新增一个文件,而不是在旧代码里"打补丁"。