Protocol Oriented Programming

从1970开始,面向对象编程(OOP)的思想就已经出现并发展了。它把对象作为程序的基本单元,包含了数据和操作数据的函数。数据封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)是OOP的三大特点。而正如OOP的迅速风靡,它具有许多面向过程等不具备的优点:

  • Encapsulation(封装)
  • Access Control(访问控制)
  • Abstraction(控制)
  • Namespace(命名空间)
  • Expressive Syntax(语法表达)
  • Extensibility(可扩展性)

但是,仔细想想,这些优点只有面向对象才有嘛?不,是抽象的类型带给我们的好处,而不是面向对象的类,类只是抽象类型的一种方式。例如在Swift中,你可以用结构体(struct)和枚举(enum)同样实现这些——除了继承和多态。但是类也有他的缺点(emm, 我不造怎么翻译了):

  • Implicit Sharing
    • Defensive Copying
    • Inefficiency
    • Race Conditions
    • Locks
    • More Inefficiency
    • Deadlock
    • Complexity
    • Bugs!
  • Inheritance All Up In Your Business
    • One superclass — choose well!
    • Single Inheritance weight gain
    • No retroactive modeling
    • Superclass may have stored properties
      • You must accept them
      • Initialization burden
      • Don’t break superclass invariants!
    • Know what/how to override (and when not to)
  • Lost Type Relationships

Implicit Sharing

在大部分的面向对象的语言中,对象都是引用类型,在对象的传递时只将引用复制一份并指向原有对象,这就带来许多问题。

class Thing {
  var name: String
  init(name: String) {
    self.name = name
  }
}

class Person {
  var things: Thing
  init(things: Thing) {
    self.things = things
  }
}

let data = Thing(name: "Data")
let A = Person(things: data)
let B = Person(things: data)
A.things.name = "Ponies"
print(B.things.name)  //"Ponies"
//B: WTF?!@##?

pop1

在上面这个例子中,很容易发现由于引用传递,B的things也被修改了。当然,解决方案也很简单,我们只需要在每次复制时默认先拷贝一份。然而,Swift对象默认没有copy方法,因为Swift更推荐使用值类型的变量而不是引用类型。不过你也可以在使用的时候自己新建一个,例如

class Thing {
  var name: String
  init(name: String) {
    self.name = name
  }
}

class Person {
  var things: Thing
  init(things: Thing) {
    self.things = Thing(name: things.name)
  }
}

let data = Thing(name: "Data")
let A = Person(things: data)
let B = Person(things: data)
A.things.name = "Ponies"
print(B.things.name)  //"Data"
//B: emm,acceptable

A和B各自拥有自己的Thing,但这不一定适用于所有的情况,并且大量的copy导致效率的下降,而且在多线程中,为了避免Race Condition(竞态条件,甚至还有 detector)而不得不加锁,导致更低的效率甚至是死锁。这些都增加了代码的复杂性同时也导致了更多的bug。

import UIKit

class Thing {
  var cnt: Int
  init(cnt: Int) {
    self.cnt = cnt
  }
  func add() {
    for _ in 0...10000 {
      cnt += 1
    }
  }
  func minus() {
    for _ in 0...10000 {
      cnt -= 1
    }
  }
}

class ViewController: UIViewController {
  let data = Thing(cnt: 0)
  @objc private func methodA() {
    data.add()
  }
  @objc private func methodB() {
   data.minus()
  }
  override func viewDidLoad() {
    super.viewDidLoad()
    let threadA = Thread(target: self, selector: #selector(methodA), object: nil)
    let threadB = Thread(target: self, selector: #selector(methodB), object: nil)
    threadA.start()
    threadB.start()
    print(data.cnt)
  }
}

// Probably not 0

所以最好我们使用值类型(e.g., struct, enum),值类型的变量在赋值时会自动进行一次低消耗的值拷贝,比对象的copy高效且线程安全。所以,这是一个理想的实现:

struct Thing {
  var name: String
  init(name: String) {
    self.name = name
  }
}

class Person {
  var things: Thing
  init(things: Thing) {
    self.things = things
  }
}

let data = Thing(name: "Data")
let A = Person(things: data)
let B = Person(things: data)
A.things.name = "Ponies"
print(B.things.name)  //"Data"
//B: awesome!

Note:

It is not safe to modify a mutable collection while enumerating through it. Some enumerators may currently allow enumeration of a collection that is modified, but this behavior is not guaranteed to be supported in the future.

正如所提示的,对enumerator对象并不推荐进行快速遍历:无论对这个对象做什么操作,对象的计数器都不会被重置,所以在删除元素的时候很容易发生越界的情况。但是,这只是Cocoa Collections,并不适用于Swift:因为Swift的Collections都是值类型。

Inheritance All Up In Your Business

类的继承只能是单继承的,所以类很容易变得臃肿复杂;需要在定义时就选择父类而不是之后的扩展中决定;如果父类有存储属性(Stored Properties),子类也不得不接受它,还要给它初始化;此外,还要知道何时/如何重写父类的方法。

Lost Type Relationships

这里举一个二分搜索的例子:

class Ordered {
  func precedes(other: Ordered) -> Bool { fatalError("implement me!") }
}

func binarySearch(sortedKeys: [Ordered], forKey k: Ordered) -> Int {
  var lo = 0, hi = sortedKeys.count
  while hi > lo {
    let mid = lo + (hi - lo) / 2
    if sortedKeys[mid].precedes(k) {
	  lo = mid + 1
    } else {
      hi = mid
    }
  }
  return lo
}

由于我们对Order的实例一无所知,所以我们实现不了这个方法,并且只能在它的子类中去实现它。

所以我们进行了如下的实现:

class Number: Ordered {
  var value: Double = 0
  override func precedes(other: Ordered) -> Bool {
    return value < other.value
  }
}

但是,这没有用。other只是个Ordered并没有value

class Number: Ordered {
  var value: Double = 0
  override func precedes(other: Ordered) -> Bool {
    return value < (other as! Number).value
  }
}

在用类进行抽象时,我们不得不写出这样的代码,多态带来的类型信息的缺失导致subClassObject as! SubClass在代码中随处可见。

Swift Is a Protocol-Oriented Programming Language

那么一种好的抽象机制是什么呢?

  • Supports value types(and classes)
  • Supports static type relationships(and dynamic dispatch)
  • Non-monolithic
  • Supports retroactive modeling
  • Doesn’t impose instance data on models
  • Doesn’t impose initialization burdens on models
  • Makes clear what to implement

而这些,正是Protocol的优点,所以Swift被设计成第一种面向协议的编程语言。

Start with a Protocol.

利用Protocol,我们修改了上述的实现:

protocol Ordered {
  func precedes(other: Ordered) -> Bool //protocol methods may not have bodies
}

struct Number: Ordered {
  var value: Double = 0
  func precedes(other: Ordered) -> Bool {
	return self.value < (other as! Number).value	
  }
}

删除了函数体之后,自然也就不是override了,所以我们把class转变成一个struct,并删掉了override关键字。这是使用Protocol的第一个可用版本,当然我们不满足于此,我们想去掉那个扎心的as!,于是我们把other类型改为Number。emm,报了一个错:

protocol Ordered {
  func precedes(other: Ordered) -> Bool
}
// Oops: protocol requires function 'precedes' with type '(Ordered) -> Bool' candidate has non-matching type '(Number) -> Bool'
struct Number: Ordered {
  var value: Double = 0
  func precedes(other: Number) -> Bool {
	return self.value < other.value	
  }
}

正如错误信息所说,这两个函数的类型不匹配呀。我们需要把protocol中的Ordered改成self。这被称为Self requirement,它只是一个在protocol中、将在之后实现该模版类型的占位符。

这也带来了另一个问题,在binarySearch的地方编译器会报一个错。

protocol ‘Ordered’ can only be used as a generic constraint because it has Self or associated type requirements

由于在函数的声明中,参数sortedKeys是个多种多样的Ordered数组,所以我们要让它homogeneous(同质的?),如下的实现表示了这是一个任一单一的Ordered类型T的一个homogeneous的数组。

也许你会认为我们这样做丢失了一些情况,而实际上我们也并没有处理那些不同种时的情况,所以这正是我们想要的。

protocol Ordered {
  func precedes(other: Self) -> Bool
}

struct Number: Ordered {
  var value: Double = 0
  func precedes(other: Number) -> Bool {
	return self.value < other.value	
  }
}

//func binarySearch(sortedKeys: [Ordered], forKey k: Ordered) -> Int {
func binarySearch<T: Ordered>(sortedKeys: [T], forKey k: T) -> Int {
  var lo = 0, hi = sortedKeys.count
  while hi > lo {
    let mid = lo + (hi - lo) / 2
    if sortedKeys[mid].precedes(k) {
	  lo = mid + 1
    } else {
      hi = mid
    }
  }
  return lo
}

Protocol Extension Tricks

我们要对IntString二分查找,所以有了如下的版本:

protocol Ordered {
  func precedes(other: Self) -> Bool
}

func binarySearch<T: Ordered>(sortedKeys: [T], forKey k: T) -> Int { ... }

extension Int: Ordered {
  func precedes(other: Int) -> Bool { return self < other }
}
extension String: Ordered {
  func precedes(other: String) -> Bool { return self < other }
}

let position = binarySearch([2, 3, 5, 7], forKey: 5)
let position = binarySearch(["2", "3", "5", "7"], forKey: "5")

而这可以把它放在Comparable里面:

protocol Ordered {
  func precedes(other: Self) -> Bool
}

func binarySearch<T: Ordered>(sortedKeys: [T], forKey k: T) -> Int { ... }

extension Comparable {
  func precedes(other: Self) -> Bool { return self < other }
}
extension Int: Ordered {}
extension String: Ordered {}
extension Double: Ordered {}

let position = binarySearch([2, 3, 5, 7], forKey: 5)
let position = binarySearch(["2", "3", "5", "7"], forKey: "5")

但这似乎有点太松了,甚至这样都能过编译:

let truth = 3.14.precedes(98.6)

但这显然不能进行二分查找。所以我们需要仅仅针对Ordered增加Comparable协议:

protocol Ordered {
  func precedes(other: Self) -> Bool
}

func binarySearch<T: Ordered>(sortedKeys: [T], forKey k: T) -> Int { ... }

extension Ordered where Self: Comparable {
  func precedes(other: Self) -> Bool { return self < other }
}

extension Int: Ordered {}
extension String: Ordered {}
let truth = 3.14.precedes(98.6)

When to Use Classes

所以什么时候用class呢?

  • 在你需要implicit sharing的时候
    • 拷贝或者比较实例没有意义(e.g., Window)
    • 实例生命周期受外部影响(e.g., 临时文件)
    • 实例像通过只写的方式流到外部状态(e.g., CGContext)
  • 别与系统做对
    • 如果框架要求你传递一个对象或者子类

pop2

Reference

WWDC15 Session408: Protocol-Oriented Programming in Swift

WWDC15 Session414: Building Better Apps with Value Types in Swift

WWDC16 Session419: Protocol and Value Oriented Programming in UIKit Apps

realm: Introduction to Protocol-Oriented MVVM

Avatar
Yang Li
@zjzsliyang

Related