面向协议编程(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 | class BVC: UIViewController { |
1.2. OOP解决方案
将
run
方法放到另一个对象A中,然后BVC、DVC
拥有对象A属性;将
run
方法添加到UIViewController
分类中(UITableViewController
继承自UIViewController
);将
run
方法抽取到新的父类,采用多继承(C++支持多继承)。
虽然可以解决问题,但是第一种方法多了一些额外的依赖关系。第二种方法会导致UIViewController
越来越臃肿,而且会影响它的其他所有子类。第三中虽然在iOS开发中用不到,但在C++中使用也会增加程序设计的复杂度,产生菱形继承等问题,需要开发者额外解决。
1.3. POP解决方案
定义一个协议,同时定义公共方法run
。扩展协议并实现抽象方法run
,类遵守协议即可。
1 | protocol Runnable { |
相比较OOP,POP的解决方案更加简洁。因为协议支持扩展实现,使得Swift使用POP非常便捷。
在Swift开发中,OOP和POP是相辅相成的,任何一方并不能取代另一方。POP能弥补OOP一些设计上的不足。
1.4. POP的注意点
- 优先考虑创建协议,而不是父类(基类)
- 优先考虑值类型(
struct、enum
),而不是引用类型(class
) - 巧用协议的扩展功能
- 不要为了面向协议而使用协议(有时候使用类更合理)
二、添加前缀
场景:为字符串增加计算纯数字的功能。
示例代码:
1 | var str = "123ttt456" |
上面的示例代码已经完成了基本的功能,但是还有很多需要优化的地方。
优化代码一(协议扩展):
1 | extension String { |
使用协议后,所有字符串都可以使用该功能,并且也限定了只有字符串类型可以使用。但是计算属性比函数调用更加优雅。
优化代码二(计算属性):
1 | extension String { |
上面优化过后看似已经很完善了,但隐藏一个风险:属性名有可能和系统有冲突。这时候我们可以为属性添加一个前缀加以区分。
优化代码三(OC风格加前缀):
1 | extension String { |
类似OC的前缀做法虽热能够满足条件,但总感觉有点OC的风格。其实我们可以使用Swift风格为属性/函数添加前缀。
优化代码四(Swift风格加前缀):
1 | struct DB { |
为字符串扩展一个DB
(自定义)属性,DB
是自定义结构体,内部实现了字符串数字计数功能。这样不仅拥有自己的命名空间,后续有任何字符串相关自定义功能都可以放到结构体中。
一般情况下,我们不会只为字符串扩展自定义功能,数组、字典、自定义类等都有可能扩展新的自定义功能。但总不能为在DB
结构体中为每一个类型都新增一个类型和初始化方法,这样DB
结构体就会变得很臃肿。可以使用泛型解决该问题。
优化代码五(通用):
1 | struct DB<Base> { |
使用泛型后,再为结构体DB
扩展对应类型的函数/属性,不仅类型隔离,还做到了统一DB
管理。
注意:为类扩展时要注意扩展方法是否要求子类也必须拥有,
Base: Person
和Base == Person
是有区别的。
如果要为类扩展属性/方法,就需要再增加一个类型属性db
。
示例代码:
1 | extension String { |
如果为不同类型扩展方法,就需要重复定义实例属性和类型属性。这时候可以使用协议实现这些属性,那个类型需要扩展方法,只需要遵守该协议即可。
最终优化代码:
1 | // 1. 前缀类型(中介) |
注意:如果扩展函数使用
mutating
,在利用协议扩展前缀属性时,属性必须是可读可写的。
为String
增加扩展功能后,NSString
和NSMutableString
也能使用么?
需要为NSString
和NSMutableString
扩展db
属性,但其实只需要为NSString
扩展就行了,因为NSMutableString
继承自NSString
。
1 | extension NSString: DBCompatible { } |
找不到对应方法,但是如果为其扩展一个和String
方法就会出现代码重复。我们知道String
、NSString
和NSMutableString
都遵守了一个ExpressibleByStringLiteral
协议,只需要把扩展条件修改一下就可以了。
1 | extension DB where Base: ExpressibleByStringLiteral { |
注意base
需要强转为String
。
三、利用协议实现类型判断
场景:判断传入的实例参数是不是数组。
示例代码:
1 | func isArray(_ value: Any) -> Bool { |
场景:判断传入的类型参数是不是数组。
示例代码:
1 | protocol ArrayType { } |
注意:[Int].Type
和[Any].Type
没有关系。协议最终是一个类型,是有自己的meta
的。