4-3.【协议导向编程】为什么 Swift “面向协议优先于面向对象”?请结合实际案例说明。

5 阅读3分钟

🧠 设计动机:解决传统 OOP 的痛点

传统面向对象编程在很多大型工程里会遇到这些问题:

  1. 继承树膨胀、僵化

    • 类层级一旦设计,就难以改动。
    • 多重行为只能靠继承链或组合,复杂且难维护。
  2. 代码复用靠父类、造成耦合

    • 子类必须继承父类才能复用逻辑。
    • 改变父类可能影响整个继承树。
  3. “能力”分散、难组合

    • 一个对象如果要同时具备多种行为(比如飞行、游泳),单纯继承不容易表达。

Swift 的核心价值:

提倡用能力(行为)来抽象,而不是用身份(类型层次)来组织代码。

这也是为什么 Swift 早期标准库大量使用协议的原因。


🛠 核心理念:协议定义行为,结构体/类组合这些行为

一个对象 能做什么 —— 用协议表示
一个对象是什么 —— 协议组合 + 类型实现

这种方式相比传统的 OOP,在 解耦、复用、可测试性 上更优雅。


🚀 实际案例对比

我们用一个具体例子来说明:
场景:实现一个通用的“可缓存对象”,支持不同策略(LRU / 带过期时间),并且可以注入到网络层进行缓存策略替换。


❌ 传统 OOP 方案(继承)

// 父类抽象缓存
class Cache {
    func get(key: String) -> Any? { return nil }
    func set(key: String, value: Any) {}
}

// LRU 缓存子类
class LRUCache: Cache {
    override func get(key: String) -> Any? {  }
    override func set(key: String, value: Any) {  }
}

// 带过期策略缓存
class ExpiringCache: Cache {
    var expirationTime: TimeInterval
    init(expirationTime: TimeInterval) {
        self.expirationTime = expirationTime
    }
    override func get(key: String) -> Any? {  }
    override func set(key: String, value: Any) {  }
}

问题:

✔️ 每种策略必须继承 Cache
✔️ 缓存行为耦合,难组合策略
❌ 没法复用“过期逻辑”给其它类型
❌ 难以创建“LRU + 过期”组合


✅ Swift 协议导向方案(POP)

// 行为协议
protocol Cacheable {
    associatedtype Value
    func get(_ key: String) -> Value?
    func set(_ key: String, _ value: Value)
}

// 默认实现可以用协议扩展
extension Cacheable {
    func logAccess(_ key: String) {
        print("[Cache] access (key)")
    }
}

// LRU策略能力
protocol LRUCacheable: Cacheable {}
extension LRUCacheable {
    func lruEvictIfNeeded() {  }
}

// 过期策略能力
protocol Expirable {
    var expirationTime: TimeInterval { get }
    func isExpired(_ key: String) -> Bool
}

// 组合策略对象
struct LRUCacheWithExpiry<Value>: LRUCacheable, Expirable {
    var expirationTime: TimeInterval
    private var storage: [String: Value] = [:]
    
    func get(_ key: String) -> Value? {
        guard !isExpired(key) else { return nil }
        return storage[key]
    }

    func set(_ key: String, _ value: Value) {
        storage[key] = value
    }
    
    func isExpired(_ key: String) -> Bool {
        // expiration logic…
    }
}

你会发现:

✔️ 行为是组合的(而非继承的)
✔️ “过期能力”可以组合到其他缓存策略
✔️ 支持策略插拔(注入到网络层)
✔️ 易测试(只依赖协议)


🧩 核心好处总结


✅ 更低耦合

协议之间是 契约,不是继承链;改动一个不用影响其他。

func fetchData<C: Cacheable>(from url: URL, cache: C) {  }

任何实现了 Cacheable 的类型都可以传入。


✅ 行为组合更自由

可以创建:

  • LRU + 过期
  • 过期 + TTL
  • 仅 LRU
  • 仅 TTL

不需要层层继承。


✅ 易测试、易 mock

protocol NetworkSession {
    func request(_ url: URL, completion: (Data?) -> Void)
}

struct MockSession: NetworkSession {
    func request(_ url: URL, completion: (Data?) -> Void) {
        completion(Data()) // mock
    }
}

只要实现协议,就可替换依赖。


✅ 更自然契合 Swift 标准库设计

举例来说:

extension Array: RangeReplaceableCollection {}

就是用协议组合提供行为,而不是让 Array 继承一大堆类。


🟡 什么时候还适合用类继承?

Swift 并不是完全否定面向对象,继承仍然有价值:

✅ UIKit / AppKit 这种 UI 组件树
✅ 需要引用语义的情况(shared mutable state)
✅ 框架中有明确层级逻辑

但这种继承更多是 实现细节,不再用于 行为抽象


🏁 总结

面向对象面向协议
重视类型层次结构重视行为(能力)抽象
继承重用逻辑协议扩展默认逻辑复用
“你是什么?”“你能做什么?”
容易耦合、难组合易组合、易测试、解耦强

Swift 团队提出 “面向协议优先于面向对象” 的原因是:

👉 协议提供更强的模块化与行为组合能力,从而提升代码灵活性、可测试性与可复用性。