<起> 协议扩展和面向协议编程

229 阅读6分钟

面向协议编程 (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 结构体通过实现 namegreet 来满足 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()
    }
}

有了这个协议扩展后,我们只需要简单地声明 ViewControllerAnotherViewController 遵守 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,但是通过 Nameablename (因为它是静态派发的),Person 依然可以遵守 Identifiable。不过,当 NameableIdentifiable 都有 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 现在的不足。

  • ✅ 动态派发安全性
  • ✅ 横切关注点
  • ❓菱形缺陷