通过类继承扩展功能时,父类逐渐变得臃肿的经历,大家应该都遇到过。为了加入公共功能,不断把方法塞进超类,最后就会面对一个让人不敢修改的巨大父类。
面向协议编程(POP)正是在这里突破OOP的局限。POP不是根据“继承了什么”,而是根据“能做什么”来设计类型。它不采用垂直的继承结构,而是按协议组合所需的能力。
本文将介绍POP源于OOP的哪些局限、实际Swift代码有何不同,以及何时适合或不适合使用。
OOP继承会在哪里受阻?
面向对象的继承确实很强大,但经常会在三个地方遇到瓶颈。
第一,单继承的局限。Swift的类只能有一个父类。要创建同时支持网络和缓存的类型,仅靠继承无法解决。
第二,臃肿基类的问题。把公共功能集中到父类后,子类连自己不用的方法也会全部继承。
第三,与值类型的适配性。Swift的struct和enum不能继承,而Swift标准库大多使用值类型。
也就是说,如果只围绕继承设计,就很难发挥Swift推崇的值类型优势。
什么是面向协议编程(POP)?
2015年,Apple在WWDC宣布“Swift是一门面向协议的语言”后,POP开始广为人知。
核心是可以为协议添加默认实现。使用协议扩展,不仅能定义接口,还能分发实际行为。
例如这样写。
protocol Greetable {
var name: String { get }
}
extension Greetable {
func greet() -> String {
return "你好, \(name)我是"
}
}
struct Person: Greetable {
let name: String
}
print(Person(name: "智勋").greet())
// 输出:你好,我是智勋
只要采纳Greetable,就能免费获得greet()。无需继承,也能把功能分配给struct。
还可以同时采纳多个协议。分别定义网络和缓存能力后,就能为需要的类型选择性地添加所需能力。
OOP继承与POP有什么不同?
两者的区别可以整理成下表。
| 项目 | OOP继承 | POP |
|---|---|---|
| 代码复用方式 | 从父类继承 | 通过协议扩展组合 |
| 类型关系 | 垂直(is-a) | 水平(can-do) |
| 多重采纳 | 仅支持单继承 | 同时采纳多个协议 |
| 值类型支持 | 不支持struct/enum | 支持struct/enum |
| 耦合度 | 父子类紧密绑定 | 按能力松散分离 |
一句话概括:
继承说明“你是什么”,协议说明“你能做什么”。
什么时候用POP,什么时候应该避免?
POP并非万能,应根据场景选择。
| 场景 | 判断 |
|---|---|
| 想为值类型(struct/enum)拆分公共功能 | POP是正确选择 |
| 需要组合不同能力 | POP更有优势 |
| 已有清晰的层级结构(动物-哺乳动物-狗) | 继承也足够好 |
| 引用共享是核心的对象(例如视图控制器) | 类继承更自然 |
| 协议拆得过细,难以追踪 | 过度抽象,需要重新审视 |
选择标准很简单:需要值类型和能力组合时用POP,需要清晰层级和引用共享时用继承。两者不是对立关系,而是可以根据场景一起使用的工具。
面试时可以这样问
Q. POP解决了OOP的哪些局限?
它解决了单继承和臃肿基类的问题。因为可以组合多个协议,只添加所需能力,还能通过协议扩展为无法继承的struct/enum等值类型提供默认实现。
Q. 协议扩展和类继承有什么区别?
继承是垂直的is-a关系,只能有一个父类;协议是水平的can-do关系,可以同时采纳多个协议。此外,继承只适用于引用类型的类,而协议也能应用于值类型。
这不是继承不好、协议就好的二分法。不过使用Swift开发时,可以先想“这能不能用协议拆分”,而不是“先上继承”。这个习惯会带来更灵活的代码。

