面向协议编程 (Protocol Oriented Programming,以下简称 POP) 是 Apple 在 2015 年 WWDC 上提出的 Swift 的一种编程范式。
相比与传统的面向对象编程 (OOP),POP 显得更加灵活。结合 Swift 的值语义特性和 Swift 标准库的实现,这一年来大家发现了很多 POP 的应用场景。本次演讲希望能在介绍 POP 思想的基础上,引入一些日常开发中可以使用 POP 的场景,让与会来宾能够开始在日常工作中尝试 POP,并改善代码设计
使用协议解决 OOP 困境
协议并不是什么新东西,也不是 Swift 的发明。在 Java 和 C# 里,它叫做 Interface。而 Swift 中的 protocol 将这个概念继承了下来,并发扬光大。让我们回到一开始定义的那个简单协议,并尝试着实现这个协议:
protocol Greetable {
var name: String { get }
func greet()
}
struct Person: Greetable {
let name: String
func greet() {
print("你好 \(name)")
}
}
Person(name: "Wei Wang").greet()
实现很简单,Person 结构体通过实现 name 和 greet 来满足 Greetable。在调用时,我们就可以使用 Greetable 中定义的方法了。
动态派发安全性
除了 Person,其他类型也可以实现 Greetable,比如 Cat:
struct Cat: Greetable {
let name: String
func greet() {
print("meow~ \(name)")
}
}
现在,我们就可以将协议作为标准类型,来对方法调用进行动态派发了:
let array: [Greetable] = [
Person(name: "Wei Wang"),
Cat(name: "onevcat")]
for obj in array {
obj.greet()
}
// 你好 Wei Wang
// meow~ onevcat
对于没有实现 Greetbale 的类型,编译器将返回错误,因此不存在消息误发送的情况:
struct Bug: Greetable {
let name: String
}
// Compiler Error:
// 'Bug' does not conform to protocol 'Greetable'
// protocol requires function 'greet()'
这样一来,动态派发安全性的问题迎刃而解。如果你保持在 Swift 的世界里,那这个你的所有代码都是安全的。
- ✅ 动态派发安全性
- 横切关注点
- 菱形缺陷
横切关注点
使用协议和协议扩展,我们可以很好地共享代码。回到上一节的 myMethod 方法,我们来看看如何使用协议来搞定它。首先,我们可以定义一个含有 myMethod 的协议:
protocol P {
func myMethod()
}
注意这个协议没有提供任何的实现。我们依然需要在实际类型遵守这个协议的时候为它提供具体的实现:
// class ViewController: UIViewController
extension ViewController: P {
func myMethod() {
doWork()
}
}
// class AnotherViewController: UITableViewController
extension AnotherViewController: P {
func myMethod() {
doWork()
}
}
你可能不禁要问,这和 Copy & Paste 的解决方式有何不同?没错,答案就是 – 没有不同。不过稍安勿躁,我们还有其他科技可以解决这个问题,那就是协议扩展。协议本身并不是很强大,只是静态类型语言的编译器保证,在很多静态语言中也有类似的概念。那到底是什么让 Swift 成为了一门协议优先的语言?真正使协议发生质变,并让大家如此关注的原因,其实是在 WWDC 2015 和 Swift 2 发布时,Apple 为协议引入了一个新特性,协议扩展,它为 Swift 语言带来了一次革命性的变化。
所谓协议扩展,就是我们可以为一个协议提供默认的实现。对于 P,可以在 extension P 中为 myMethod 添加一个实现:
protocol P {
func myMethod()
}
extension P {
func myMethod() {
doWork()
}
}
有了这个协议扩展后,我们只需要简单地声明 ViewController 和 AnotherViewController 遵守 P,就可以直接使用 myMethod 的实现了:
extension ViewController: P { }
extension AnotherViewController: P { }
viewController.myMethod()
anotherViewController.myMethod()
不仅如此,除了已经定义过的方法,我们甚至可以在扩展中添加协议里没有定义过的方法。在这些额外的方法中,我们可以依赖协议定义过的方法进行操作。我们之后会看到更多的例子。总结下来:
- 协议定义
- 提供实现的入口
- 遵循协议的类型需要对其进行实现
- 协议扩展
- 为入口提供默认实现
- 根据入口提供额外实现
这样一来,横切点关注的问题也简单安全地得到了解决。
- ✅ 动态派发安全性
- ✅ 横切关注点
- 菱形缺陷
菱形缺陷
最后我们看看多继承。多继承中存在的一个重要问题是菱形缺陷,也就是子类无法确定使用哪个父类的方法。在协议的对应方面,这个问题虽然依然存在,但却是可以唯一安全地确定的。我们来看一个多个协议中出现同名元素的例子:
protocol Nameable {
var name: String { get }
}
protocol Identifiable {
var name: String { get }
var id: Int { get }
}
如果有一个类型,需要同时实现两个协议的话,它必须提供一个 name 属性,来同时满足两个协议的要求:
struct Person: Nameable, Identifiable {
let name: String
let id: Int
}
// `name` 属性同时满足 Nameable 和 Identifiable 的 name
这里比较有意思,又有点让人困惑的是,如果我们为其中的某个协议进行了扩展,在其中提供了默认的 name 实现,会如何。考虑下面的代码:
extension Nameable {
var name: String { return "default name" }
}
struct Person: Nameable, Identifiable {
// let name: String
let id: Int
}
// Identifiable 也将使用 Nameable extension 中的 name
这样的编译是可以通过的,虽然 Person 中没有定义 name,但是通过 Nameable 的 name (因为它是静态派发的),Person 依然可以遵守 Identifiable。不过,当 Nameable 和 Identifiable 都有 name 的协议扩展的话,就无法编译了:
extension Nameable {
var name: String { return "default name" }
}
extension Identifiable {
var name: String { return "another default name" }
}
struct Person: Nameable, Identifiable {
// let name: String
let id: Int
}
// 无法编译,name 属性冲突
这种情况下,Person 无法确定要使用哪个协议扩展中 name 的定义。在同时实现两个含有同名元素的协议,并且它们都提供了默认扩展时,我们需要在具体的类型中明确地提供实现。这里我们将 Person 中的 name 进行实现就可以了:
extension Nameable {
var name: String { return "default name" }
}
extension Identifiable {
var name: String { return "another default name" }
}
struct Person: Nameable, Identifiable {
let name: String
let id: Int
}
Person(name: "onevcat", id: 123).name // onevcat
这里的行为看起来和菱形问题很像,但是有一些本质不同。首先,这个问题出现的前提条件是同名元素以及同时提供了实现,而协议扩展对于协议本身来说并不是必须的。其次,我们在具体类型中提供的实现一定是安全和确定的。当然,菱形缺陷没有被完全解决,Swift 还不能很好地处理多个协议的冲突,这是 Swift 现在的不足。
- ✅ 动态派发安全性
- ✅ 横切关注点
- ❓菱形缺陷