深入理解 SOLID 原则:用 Swift 编写优雅、可维护的代码

从理论到实战,逐条拆解 + 代码示例 + 重构案例,让你一次掌握五大设计原则。

什么是 SOLID?

SOLID 是面向对象设计的五大原则,帮助开发者写出高内聚、低耦合、易测试、可扩展的代码。

  • Single Responsibility Principle(单一职责)
  • Open/Closed Principle(开闭原则)
  • Liskov Substitution Principle(里氏替换)
  • Interface Segregation Principle(接口隔离)
  • Dependency Inversion Principle(依赖反转)

SRP:单一职责原则

一个类/模块只有一个理由去改变

把"因为需求 A 修改"和"因为需求 B 修改"拆成两个类。

❌ 反面示例

swift 复制代码
class DataManager {
    func fetchData() { /* 网络请求 */ }
    func parseData(_ data: Data) { /* JSON 解析 */ }
    func saveData(_ parsed: ParsedData) { /* 写入磁盘 */ }
    func showAlert() { /* UI 弹窗 */ }   // 又管 UI?
}

✅ 重构后:职责分离

swift 复制代码
// 网络
class NetworkService {
    func fetchData() async throws -> Data { /* ... */ }
}

// 解析
class JSONParser {
    func parse<T: Decodable>(_ data: Data) throws -> T { /* ... */ }
}

// 存储
class CacheService {
    func save<T: Encodable>(_ object: T, key: String) throws { /* ... */ }
}
  • 好处:修改网络层时不会波及缓存逻辑,单元测试也更容易 mock。

OCP:开闭原则

对扩展开放,对修改关闭

加功能时尽量不改动旧代码,用协议 + 多态解决。

❌ 反面:每加一个形状就改 switch

swift 复制代码
enum ShapeType { case circle, square }
func render(_ type: ShapeType) {
    switch type {
    case .circle: /* 画圆 */
    case .square: /* 画方 */
    }
}

✅ 重构:面向协议扩展

swift 复制代码
protocol Renderer {
    func render()
}

struct CircleRenderer: Renderer {
    func render() { /* 画圆 */ }
}

struct SquareRenderer: Renderer {
    func render() { /* 画方 */ }
}

// 新增三角形:零改动旧代码
struct TriangleRenderer: Renderer {
    func render() { /* 画三角形 */ }
}
  • 好处:新增形状只需再写一个 Renderer,老代码稳如老狗。

LSP:里氏替换原则

子类必须能透明替换父类,不能破坏父类约定。

❌ 反面:正方形继承矩形导致异常

swift 复制代码
class Rectangle {
    var width: Double = 0
    var height: Double = 0
    func area() -> Double { width * height }
}

class Square: Rectangle {
    override var width: Double {
        didSet { height = width }   // 破坏父类行为
    }
}

func printArea(_ r: Rectangle) {
    r.width = 5
    r.height = 4
    print(r.area()) // 期望 20,Square 却得到 16
}

✅ 重构:协议抽象

swift 复制代码
protocol Shape {
    func area() -> Double
}

struct Rectangle: Shape {
    let width, height: Double
    func area() -> Double { width * height }
}

struct Square: Shape {
    let side: Double
    func area() -> Double { side * side }
}
  • 好处:Square 无需伪装成 Rectangle,行为自然正确。

ISP:接口隔离原则

客户端不应被迫依赖它不需要的接口

把胖协议拆成小协议,按需组合。

❌ 反面:胖协议

swift 复制代码
protocol Database {
    func save()
    func fetch()
    func delete()
    func backup()
}

class TinyCache: Database {
    func save() { /* 只用到 save */ }
    func fetch() { /* 空实现 */ }
    func delete() { /* 空实现 */ }
    func backup() { /* 空实现 */ } // 被迫实现
}

✅ 重构:小协议 + 组合

swift 复制代码
protocol Savable {
    func save()
}

protocol Fetchable {
    func fetch() -> Data
}

class TinyCache: Savable {
    func save() { /* 只需实现 save */ }
}
  • 好处:实现类只关心自己需要的接口,避免"大而全"。

DIP:依赖反转原则

高层模块不依赖低层细节,两者都依赖抽象

通过构造函数/属性注入解耦。

❌ 反面:硬编码依赖

swift 复制代码
class DataManager {
    private let api = RemoteAPI() // 写死
    func load() {
        let data = api.fetch()
    }
}
  • 测试时想换成 MockAPI 很难。

✅ 重构:依赖注入

swift 复制代码
protocol DataProvider {
    func fetch() async throws -> Data
}

struct RemoteAPI: DataProvider { /* ... */ }
struct MockAPI: DataProvider { /* ... */ }

class DataManager {
    private let provider: DataProvider
    init(provider: DataProvider) {
        self.provider = provider
    }
    func load() async throws {
        let data = try await provider.fetch()
    }
}

// 使用
let manager = DataManager(provider: RemoteAPI())
// 测试
let testManager = DataManager(provider: MockAPI())
  • 好处:随时替换实现,单元测试秒变 mock。

综合案例:重构一个"用户服务"

需求

  • 登录后获取用户信息
  • 缓存到本地
  • 支持远程 & 本地两种数据源

初始代码(违反 SRP+DIP)

swift 复制代码
class UserService {
    func login() {
        // 网络请求
        // JSON 解析
        // 写磁盘
        // 通知 UI
    }
}

✅ SOLID 重构后

swift 复制代码
// 1. 抽象数据源
protocol UserDataSource {
    func fetchUser(id: String) async throws -> User
}

// 2. 实现
struct RemoteUserSource: UserDataSource { /* 网络 */ }
struct LocalUserSource: UserDataSource { /* 缓存 */ }

// 3. 解析器
struct UserParser {
    func parse(_ data: Data) throws -> User { /* ... */ }
}

// 4. 仓库(高层模块)
class UserRepository {
    private let remote: UserDataSource
    private let local: UserDataSource
    private let parser: UserParser
    
    init(remote: UserDataSource, local: UserDataSource, parser: UserParser) {
        self.remote = remote
        self.local = local
        self.parser = parser
    }
    
    func user(id: String) async throws -> User {
        if let cached = try await local.fetchUser(id: id) { return cached }
        let data = try await remote.fetchUser(id: id)
        return try parser.parse(data)
    }
}
  • 单一职责:每个类只做一件事
  • 开闭原则:新增 GraphQLUserSource 不改动旧代码
  • 依赖反转:UserRepository 只依赖 UserDataSource 协议

总结速记表

原则 一句话 Swift 技巧
SRP 一个类做一件事 拆小类、用协议
OCP 加功能不改动旧代码 协议 + 多态
LSP 子类必须可替换父类 用协议代替继承
ISP 接口要小而专 小协议组合
DIP 依赖抽象不依赖细节 构造函数注入
相关推荐
HarderCoder4 小时前
Swift 数据容器全景手册:Sequence、Collection、Set、Dictionary 一次掌握
swift
HarderCoder5 小时前
Swift 并发全景指南:Thread、Concurrency、Parallelism 一次搞懂
swift
HarderCoder6 小时前
Swift 并发模型深度解析:Singleton 与 Global Actor 如何抉择?
swift
HarderCoder9 小时前
Swift Global Actor 完全指南
swift
HarderCoder9 小时前
Swift 计算属性(Computed Property)详解:原理、性能与实战
swift
HarderCoder9 小时前
Swift Property Wrapper:优雅地消除样板代码
swift
东坡肘子11 小时前
未来将至:人形机器人运动会 | 肘子的 Swift 周报 #099
swiftui·swift·apple
大熊猫侯佩1 天前
反抗军工程师的 “苹果智能” 实战指南:用本机基础模型打造 AI 利刃
ai编程·swift·apple
YungFan2 天前
iOS26适配指南之UIViewController
ios·swift