【iOS】Swift系列三十三 - 面向协议编程

面向协议编程(Protocol Oriented Programming,简称POP)是Swift的一种编程范式,Apple于2015年WWDC提出。在Swift的标准库中,能见到大量POP的影子。

一、POP和OOP

1.1. 回顾OOP

Swift也是一门面向对象的编程语言(Object Oriented Programming,简称OOP)。

OOP的三大特性:封装、继承、多态。

继承的经典使用场景:当多个类(比如A、B、C类)具有很多共性时,可以将这些共性抽取到一个父类中(比如D类),最后A、B、C类继承D类。

有些问题,使用OOP并不能很好解决,比如:如何将BVC、DVC的公共方法run抽取出来?

1
2
3
4
5
6
7
8
9
10
11
class BVC: UIViewController {
func run() {
print("run")
}
}

class DVC: UITableViewController {
func run() {
print("run")
}
}

1.2. OOP解决方案

  1. run方法放到另一个对象A中,然后BVC、DVC拥有对象A属性;

  2. run方法添加到UIViewController分类中(UITableViewController继承自UIViewController);

  3. run方法抽取到新的父类,采用多继承(C++支持多继承)。

虽然可以解决问题,但是第一种方法多了一些额外的依赖关系。第二种方法会导致UIViewController越来越臃肿,而且会影响它的其他所有子类。第三中虽然在iOS开发中用不到,但在C++中使用也会增加程序设计的复杂度,产生菱形继承等问题,需要开发者额外解决。

1.3. POP解决方案

定义一个协议,同时定义公共方法run。扩展协议并实现抽象方法run,类遵守协议即可。

1
2
3
4
5
6
7
8
9
10
11
12
protocol Runnable {
func run()
}

extension Runnable {
func run() {
print("run")
}
}

class BVC: UIViewController, Runnable { }
class DVC: UITableViewController, Runnable { }

相比较OOP,POP的解决方案更加简洁。因为协议支持扩展实现,使得Swift使用POP非常便捷。

在Swift开发中,OOP和POP是相辅相成的,任何一方并不能取代另一方。POP能弥补OOP一些设计上的不足。

1.4. POP的注意点

  • 优先考虑创建协议,而不是父类(基类)
  • 优先考虑值类型(struct、enum),而不是引用类型(class
  • 巧用协议的扩展功能
  • 不要为了面向协议而使用协议(有时候使用类更合理)

二、添加前缀

场景:为字符串增加计算纯数字的功能。

示例代码:

1
2
3
4
5
6
7
8
9
var str = "123ttt456"
func numberCount(_ str: String) -> Int {
var count = 0
for c in str where ("0"..."9").contains(c) {
count += 1
}
return count
}
print(numberCount(str)) // 输出:6

上面的示例代码已经完成了基本的功能,但是还有很多需要优化的地方。

优化代码一(协议扩展):

1
2
3
4
5
6
7
8
9
10
extension String {
func numberCount() -> Int {
var count = 0
for c in self where ("0"..."9").contains(c) {
count += 1
}
return count
}
}
print(str.numberCount()) // 输出:6

使用协议后,所有字符串都可以使用该功能,并且也限定了只有字符串类型可以使用。但是计算属性比函数调用更加优雅。

优化代码二(计算属性):

1
2
3
4
5
6
7
8
9
10
11
extension String {
var numberCount: Int {
var count = 0
for c in self where ("0"..."9").contains(c) {
count += 1
}
return count
}
}
print(str.numberCount) // 输出:6
print("45579test12".numberCount) // 输出:7

上面优化过后看似已经很完善了,但隐藏一个风险:属性名有可能和系统有冲突。这时候我们可以为属性添加一个前缀加以区分。

优化代码三(OC风格加前缀):

1
2
3
4
5
6
7
8
9
10
extension String {
var db_numberCount: Int {
var count = 0
for c in self where ("0"..."9").contains(c) {
count += 1
}
return count
}
}
print(str.db_numberCount) // 输出:6

类似OC的前缀做法虽热能够满足条件,但总感觉有点OC的风格。其实我们可以使用Swift风格为属性/函数添加前缀。

优化代码四(Swift风格加前缀):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct DB {
var string: String
init(_ string: String) {
self.string = string
}
var numberCount: Int {
var count = 0
for c in string where ("0"..."9").contains(c) {
count += 1
}
return count
}
}

extension String {
var db: DB {
return DB(self)
}
}
print(str.db.numberCount) // 输出:6

为字符串扩展一个DB(自定义)属性,DB是自定义结构体,内部实现了字符串数字计数功能。这样不仅拥有自己的命名空间,后续有任何字符串相关自定义功能都可以放到结构体中。

一般情况下,我们不会只为字符串扩展自定义功能,数组、字典、自定义类等都有可能扩展新的自定义功能。但总不能为在DB结构体中为每一个类型都新增一个类型和初始化方法,这样DB结构体就会变得很臃肿。可以使用泛型解决该问题。

优化代码五(通用):

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
34
35
36
37
38
39
struct DB<Base> {
var base: Base
init(_ base: Base) {
self.base = base
}
}

extension String {
var db: DB<String> { DB(self) }
}

class Person {
var name: String
init(_ name: String) {
self.name = name
}
}
extension Person {
var db: DB<Person> { DB(self) }
}

extension DB where Base == String {
var numberCount: Int {
var count = 0
for c in base where ("0"..."9").contains(c) {
count += 1
}
return count
}
}
print(str.db.numberCount) // 输出:6

extension DB where Base: Person {
func run() {
print(base.name, "running")
}
}
let p = Person("大奔")
print(p.db.run()) // 输出:大奔 running

使用泛型后,再为结构体DB扩展对应类型的函数/属性,不仅类型隔离,还做到了统一DB管理。

注意:为类扩展时要注意扩展方法是否要求子类也必须拥有,Base: PersonBase == Person是有区别的。

如果要为类扩展属性/方法,就需要再增加一个类型属性db

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extension String {
var db: DB<String> { DB(self) }
static var db: DB<String>.Type { DB<String>.self }
}

extension DB where Base == String {
var numberCount: Int {
var count = 0
for c in base where ("0"..."9").contains(c) {
count += 1
}
return count
}
static func test() {
print("String test")
}
}
String.db.test() // 输出:String test

如果为不同类型扩展方法,就需要重复定义实例属性和类型属性。这时候可以使用协议实现这些属性,那个类型需要扩展方法,只需要遵守该协议即可。

最终优化代码:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 1. 前缀类型(中介)
struct DB<Base> {
var base: Base
init(_ base: Base) {
self.base = base
}
}

// 2. 利用协议扩展前缀属性
protocol DBCompatible { }
extension DBCompatible {
var db: DB<Self> {
set { }
get { DB(self) }
}
static var db: DB<Self>.Type {
set { }
get { DB<Self>.self }
}
}

// 3. 让String拥有db前缀属性
extension String: DBCompatible { }

// 4. 给db前缀(实例/类型)扩展功能
extension DB where Base == String {
var numberCount: Int {
var count = 0
for c in base where ("0"..."9").contains(c) {
count += 1
}
return count
}
static func test() {
print("String test")
}
}
print(str.db.numberCount) // 输出:6
String.db.test() // 输出:String test

// 为自定义类扩展方法
class Person {
var name: String
init(_ name: String) {
self.name = name
}
}

extension Person: DBCompatible { }

extension DB where Base: Person {
func run() {
print(base.name, "running")
}
}
let p = Person("大奔")
print(p.db.run()) // 输出:大奔 running

注意:如果扩展函数使用mutating,在利用协议扩展前缀属性时,属性必须是可读可写的。

String增加扩展功能后,NSStringNSMutableString也能使用么?

需要为NSStringNSMutableString扩展db属性,但其实只需要为NSString扩展就行了,因为NSMutableString继承自NSString

1
extension NSString: DBCompatible { }

找不到对应方法,但是如果为其扩展一个和String方法就会出现代码重复。我们知道StringNSStringNSMutableString都遵守了一个ExpressibleByStringLiteral协议,只需要把扩展条件修改一下就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
extension DB where Base: ExpressibleByStringLiteral {
var numberCount: Int {
var count = 0
for c in (base as! String) where ("0"..."9").contains(c) {
count += 1
}
return count
}
static func test() {
print("String test")
}

}

注意base需要强转为String

三、利用协议实现类型判断

场景:判断传入的实例参数是不是数组。

示例代码:

1
2
3
4
5
6
7
8
func isArray(_ value: Any) -> Bool {
value is [Any]
}
print(isArray([1, 2])) // 输出:true
print(isArray(["1", 2])) // 输出:true
print(isArray(NSArray())) // 输出:true
print(isArray(NSMutableArray())) // 输出:true
print(isArray("123")) // 输出:false

场景:判断传入的类型参数是不是数组。

示例代码:

1
2
3
4
5
6
7
8
9
10
protocol ArrayType { }
extension Array: ArrayType { }
extension NSArray: ArrayType { }
func isArrayType(_ type: Any.Type) -> Bool {
type is ArrayType.Type
}
print(isArrayType([Int].self)) // 输出:true
print(isArrayType([Any].self)) // 输出:true
print(isArrayType(NSArray.self)) // 输出:true
print(isArrayType(NSMutableArray.self)) // 输出:true

注意:[Int].Type[Any].Type没有关系。协议最终是一个类型,是有自己的meta的。