4-5.【协议导向编程】在设计大型系统时,如何在性能和抽象之间平衡 POP 的使用?

6 阅读2分钟

一、先给结论(工程经验版)

POP 用在「边界」,Concrete 用在「核心」

  • 系统边界:协议优先(解耦、替换、测试)
  • 性能核心路径:具体类型 + 泛型 + 静态派发
  • UI / 业务层:协议适度,避免“协议滥用”
  • 热路径:避免 protocol existential(any Protocol

这句话你记住,后面都是展开解释。


二、为什么 POP 会“伤性能”?(说清楚成本)

1️⃣ Protocol Existential = 动态派发

func draw(_ d: any Drawable) {
    d.draw()
}

👉 编译器只能走 witness table
👉 无法内联
👉 有额外间接调用

代价

  • 多一次 indirection
  • 破坏内联与优化
  • 热路径上会被放大

2️⃣ 泛型 + 协议约束 = 静态派发(快)

func draw<T: Drawable>(_ d: T) {
    d.draw()
}

👉 编译期就知道类型
👉 可内联
👉 零成本抽象(接近)

📌 这是 Swift 团队真正推荐的 POP 用法


三、一个真实架构案例:日志系统

❌ 天真 POP(性能差 + 过度抽象)

protocol Logger {
    func log(_ message: String)
}

struct FileLogger: Logger { ... }
struct ConsoleLogger: Logger { ... }

let logger: any Logger = FileLogger()

for _ in 0..<1_000_000 {
    logger.log("hi")
}

🚨 热路径 + existential = 性能雷区


✅ 工程化 POP(边界抽象 + 核心具体)

1️⃣ 抽象边界(系统入口)

protocol Logger {
    func log(_ message: String)
}

2️⃣ 核心实现(具体类型)

struct FileLogger: Logger {
    func log(_ message: String) { /* fast path */ }
}

3️⃣ 泛型包裹(静态派发)

struct LogService<L: Logger> {
    let logger: L

    func log(_ message: String) {
        logger.log(message)
    }
}

4️⃣ 使用

let service = LogService(logger: FileLogger())

service.log("hi") // 静态派发

📈 性能接近直接调用
📦 抽象仍然存在
🧪 测试可替换


四、设计决策表(你可以直接照这个做)

场景推荐方式
模块边界协议
核心算法struct / enum
热路径泛型约束
插件系统existential
单元测试协议
UI 层协议 + class
库 / Framework协议 + 默认实现

五、Swift 团队的真实用法(非常关键)

标准库的套路:

extension Array: RandomAccessCollection {}
  • 协议定义能力
  • 具体类型实现
  • 泛型算法使用协议
func quickSort<C: RandomAccessCollection>(_ c: C) { ... }

❌ 他们几乎不会写:

func quickSort(_ c: any RandomAccessCollection)

这就是答案。


六、什么时候该“放弃 POP”?

🚫 明确不适合 POP 的地方

  • 数值计算 / 图形 / 音频
  • tight loop
  • 高频 IO
  • 编解码核心逻辑

👉 用 具体类型 + 值语义


七、一个黄金判断问题(超实用)

每次你想加一个协议,问自己一句话:

“我真的需要在运行时替换这个实现吗?”

  • ❌ 不需要 → 泛型 / concrete type
  • ✅ 需要 → protocol + existential
  • 🤔 未来可能 → 协议 + 泛型封装

八、最终口诀(送你一个)

协议用于「解耦」
泛型用于「性能」
existential 用于「灵活性」
具体类型用于「极致速度」

这四个你能分清,Swift 架构就已经是高段位了。