Farlanki.

漫谈Swift面向协议编程

字数统计: 2.8k阅读时长: 10 min
2017/03/13 Share

前言

面向对象编程(OOP)自从在很久之前就已经成为了主流,相信每一个软件工程师都曾经或者正在使用着OOP编程.诚然,OOP为我们提供了很多很好的特性,例如封装,多态和继承等,让代码复用率更高,程序的解耦更加容易实现.但是,在随着软件的功能越来越多,需求越来越复杂,OOP的一些缺点已经被暴露了出来.

破立

OOP缺点一:结构复杂

很明显,OOP已经流行了很长的一段时间.在很多情况下,每当我们要实现一个新的需求,我们总会想到创建一个或几个新的类.这些类或许继承了之前已经存在的类,或者把之前存在的一些类的对象Wrap起来,从而使用它们的功能.然而这样做,很多情况下我们的类会继承到一些我们并用不到的功能.或者我们会不清楚到底应该把子类放在整个类结构的哪个位置.
考虑这个情况:


Android Phone,iOS Phone继承自mobile phone,S7继承自Android Phone,iPhone 7继承自iOS Phone.一切运作得良好,直到一天我们需要将Windows Phone纳入考虑.


只需要稍微将这个结构扩展一下,就可以达到这个需求.完全没有问题.
直到有一天,我们需要将平板电脑纳入考虑.


到底我们应该怎么设计这个结构呢?难道再设计一个mobile phone 和tablet的共同父类吗?那样会导致iOS中有部分代码破坏了don’t repeat yourself原则.把整个hierarchy的结构调整一下吗?


so far so good.直到有一天运行着Android的智能汽车出现了,整个hierarchy又不得不进行一番大改革.
OOP的一个很大的缺点就体现在这里,子类默认把父类中的属性和方法都全部继承下来,整个hierarchy十分严格,以至于需求出现一些变化我们就不得不对hierarchy进行调整.

OOP缺点二:隐式共享


由于在Swift中类是引用类型的,所以当对象A被赋值给对象B的时候,实际上只是引用被复制到B.但是对象A和对象B都对对方毫不知情.所以,我们很容易写出经由对象A或者对象B改变底层数据的情况.实际上这样做完全符合直觉,但是却因为OOP的不足而造成了错误.

如果使用POP呢


当我们选择使用POP的时候,结构变成了这样子.有没有一种”扁平化”的感觉?和OOP中的继承不同,POP中强调的是”组装“.当新的需求来到,只需要把不用的协议组装在一起,就形成了一个新的实例.

哪里体现了多态性:协议类型

有人可能认为:我们花了这么大力气尝试把各种不同的设备组合成一个层次分明的hierarchy,其中一个很重要的原因就是因为只需要这些子类有一个共同的一个父类,那么我们就可以使用OOP的多态性,对某个对象的方法进行动态决议,做到调用代码一致,但是运行结果因类而异的情况.简单的来说,就是让这些操作变成可能:

1
2
3
4
5
6
7
var deviceArray : [SmartProtableDevice] = []

///add device to deviceArray

for device in deviceArray{
device.reboot()//假设SmartProtableDevice中实现了reboot方法.
}

在swift中,协议也体现了多态性.
先说点swift中的黑魔法.观察上面的图,我们发现这五种设备都没有一个公共的协议,怎么做到多态性呢?

1
2
3
4
5
6
7
protocol RebootAble{
func reboot()
}
protocol iOSRunable{
//some function
}
typealias iOSWorkable = RebootAble & iOSRunable

首先,把iOSWorkable拆分成两个粒度更小的协议,Android和Windows也一样进行拆分(无论Android,Windows和iOS都必定可以重启哈哈).然后每种设备都遵循了RebootAble这个协议.

1
2
3
4
5
6
7
var deviceArray : [RebootAble] = []

///add device to deviceArray

for device in deviceArray{
device.reboot()//因为设备遵循RebootAble协议,所以拥有reboot方法.
}

这两段代码体现了两点:

  1. Swift中协议也是一种类型,依靠这点可以实现多态性
  2. Swift中协议可以很轻易的拆分成更多粒度更小的协议,而遵守这个协议的类可以对此毫不知情,一切改动只发生在协议的定义中.

另一个栗子

举一个更实际的例子.假如现在需要实现一个独特的按钮类,需要按钮在被点击的时候执行两个效果:
1.按钮的边缘出现炫光.
2.按钮震动.
机制的程序员当然会从github上找答案.现在你找到了两个类:一个是继承自UIButton实现了边缘炫光的按钮类,另一个也是继承自UIButton但是实现了震动效果的按钮类.问题是:怎么才能将两种效果组合在一起呢?难道需要我们深入这两个类的源码,找到实现这两种效果的方法,然后在粘贴到我们自己实现的类中呢?还是先继承一个类,再把另一个类的方法复制过来?很明显,这两种实现方法都并不优雅.
但是,如果这两个库都使用了POP的话,那么我们就可以创建自己的一个类或者结构体,只需要将该类或者结构体实现了对应的协议,那么就可以拥有这两个效果了.

哪里体现了代码复用:protocol extension

可能已经有读者意识到了一个问题:既然我们的类需要符合两个协议,那么我们便需要为这个类编写这两个协议中相应方法的实现,到头来实现部分还不是我干的吗?那究竟这两个库做了些什么?
Swift被称为世界上第一个面向协议编程的语言的强大之处就是它可以为协议提供扩展,协议的扩展可以提供具体方法的实现,体现了重用性.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protocol LightAble{
func light()
}
protocol ShakeAble{
func shake()
}

extension LightAble{
func light(){
//implementation of light
}
}

extension ShakeAble{
func shake(){
//implementation of shake
}
}

class MyButton : UIButton , LightAble , ShakeAble{
//call shake() and light() in a function
}

利用POP优化identifier使用体验

在编写iOS App的时候我们不得不和各种identifier打交道,例如segue identifier 和 reuse identifier.这些identifier都是通过字符串传递的,使用的时候编译器并不知道我们传递的identifier究竟是否真的存在.或许我们的identifier存在一个拼写错误,那么就很容易导致应用程序崩溃.

1
2
3
4
5
6
7
8
9
10
11
// ViewController.swift

@IBAction func onRedPillButtonTap(sender: AnyObject) {
// I'm hard-coding my Red Pill segue identifier here 😬
performSegueWithIdentifier("TheRedPillExperience", sender: self)
}

@IBAction func onBluePillButtonTap(sender: AnyObject) {
// I'm hard-coding my Blue Pill segue identifier again here 😬
performSegueWithIdentifier("TheBluePillExperience", sender: self)
}

当然,我们可以在ViewController中声明一个枚举类型以储存identifier.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ViewController.swift
enum SegueIdentifier: String {
case TheRedPillExperience
case TheBluePillExperience
}

@IBAction func onRedPillButtonTap(sender: AnyObject) {
// so this is pretty long...
performSegueWithIdentifier(SegueIdentifier.TheRedPillExperience.rawValue, sender: self)
}

@IBAction func onBluePillButtonTap(sender: AnyObject) {
// and so is this...
performSegueWithIdentifier(SegueIdentifier.TheBluePillExperience.rawValue, sender: self)
}

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

guard let identifier = segue.identifier,
segueIdentifier = SegueIdentifier(rawValue: identifier) else {
fatalError("Invalid segue identifier \(segue.identifier)."
}

switch segueIdentifier {
case .TheRedPillExperience:
print("😈")
case .TheBluePillExperience:
print("👼")
}
}

很明显,如果我们有很多的ViewController,那么这部分代码是逃不过被重复的命运的了,很明显违背了don’t repeat原则,体现不了代码复用.一个优雅的方法是使用POP处理这些identifier.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protocol SegueHandlerType {
typealias SegueIdentifier: RawRepresentable
}

extension SegueHandlerType where Self: UIViewController,
SegueIdentifier.RawValue == String
{

func performSegueWithIdentifier(segueIdentifier: SegueIdentifier,
sender: AnyObject?) {

performSegueWithIdentifier(segueIdentifier.rawValue, sender: sender)
}

func segueIdentifierForSegue(segue: UIStoryboardSegue) -> SegueIdentifier {

// still have to use guard stuff here, but at least you're
// extracting it this time
guard let identifier = segue.identifier,
segueIdentifier = SegueIdentifier(rawValue: identifier) else {
fatalError("Invalid segue identifier \(segue.identifier).") }

return segueIdentifier
}
}

注意这里运用了protocol extension以达到代码复用的目的.protocol要求遵循它的类定义一个遵循了RawRepresentable协议的SegueIdentifier.枚举类型的变量就是遵循RawRepresentable的变量.但是很明显不是每一个ViewController都需要遵循这个协议,于是利用了where关键字对范围进行了限制.
需要用到segue时,只需要简单调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class ViewController: UIViewController, SegueHandlerType {

// the compiler will now complain if you don't have this implemented
// you need this to conform to SegueHandlerType
enum SegueIdentifier: String {
case TheRedPillExperience
case TheBluePillExperience
}

override func viewDidLoad() {
super.viewDidLoad()
}

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

// 🎉 goodbye pyramid of doom!
switch segueIdentifierForSegue(segue) {
case .TheRedPillExperience:
print("😈")
case .TheBluePillExperience:
print("👼")
}
}

@IBAction func onRedPillButtonTap(sender: AnyObject) {
// ✅ this is how I want to write my code! Beautiful!
performSegueWithIdentifier(.TheRedPillExperience, sender: self)
}

@IBAction func onBluePillButtonTap(sender: AnyObject) {
performSegueWithIdentifier(.TheBluePillExperience, sender: self)
}
}

可以看到,使用.Case这种方式就可以达到调用performSegueWithIdentifier的效果.这是因为SegueHandlerType这个协议为原本的performSegueWithIdentifier方法增加了一层抽象,告诉编译器performSegueWithIdentifier的第一个参数是SegueIdentifier类型的,在协议内部再调用系统的performSegueWithIdentifier方法.利用增加的一层,我们告诉了编译器更多信息,那么我们在真正写代码的时候,就可以写出更少的代码,让编译器为我们做更多的事.

关于性能

说了很多protocol的好处,难道它就没有缺点吗?并不是.Swift为了实现POP,使用了协议类型.就是协议类型为我们提供了多态性.
我们知道,被放入数组的数据结构需要有同样的大小.为实现这点,协议类型的模型其实是一个Wrapper:


pwt中记录着协议所规定方法的实现,valueBuffer中可以存放小于三个字节的数据.如果数据大于三个字节,那么valueBuffer存放的是指向这个数据结构的指针,指向堆.很明显,当使用协议类型的是引用类型,那么valueBuffer中存放的一定是指针.


是否使用协议类型,valueBuffer中存放的是否是对象本身对性能影响极大:


以上三种方式分别是不使用协议类型,使用协议类型并且数据存放在buffer中, 使用协议类型并且数据存放在堆中的例子,运行时间分别为0.3秒,4秒,45秒.

总结

Swift的POP为我们打开了一道新世界的大门,POP为编程引入一种”组装”的思想.对比OOP,POP能在很多情况下为我们带来更简洁的hierachy.通过协议扩展和协议类型,Swift实现了面向协议的多态性和良好的代码复用原则.当然,协议类型在某些情况下会带来不小的性能问题,特别是在数据结构特别多的时候,所以在这种情况下应该谨慎的使用POP.

参考资料

如何开发一个流行的 Swift 开源动画库 - IBAnimatable
Swift 面向协议编程作者揭秘 Swift 世界的黑与白
Protocol-Oriented Segue Identifiers in Swift
Swift代码性能优化

CATALOG
  1. 1. 前言
  2. 2. 破立
    1. 2.1. OOP缺点一:结构复杂
    2. 2.2. OOP缺点二:隐式共享
    3. 2.3. 如果使用POP呢
      1. 2.3.1. 哪里体现了多态性:协议类型
    4. 2.4. 另一个栗子
      1. 2.4.1. 哪里体现了代码复用:protocol extension
  3. 3. 利用POP优化identifier使用体验
  4. 4. 关于性能
  5. 5. 总结
    1. 5.1. 参考资料