从编程范式讲起
或许你经常听到诸如面向对象编程、面向过程编程、面向协议编程、函数式编程这些词,心中也不免疑惑,这些词都是些啥?
编程范式 on wiki
相比于本文要介绍的面向协议编程,面向对象编程的名声似乎更响。面向对象由于C++和java的流行,成为近二十年来最为流行的**编程范式。其他编程范式还有比面向对象更早的面向过程编程**、指令编程,以及新兴的以Haskell为代表的**函数式编程和苹果提出的面向协议*编程。
编程范式是一类典型的编程风格,是指从事软件工程的一类典型的风格(可以对照方法学)。编程范型提供了(同时决定了)程序员对程序执行的看法。 ——wiki
我理解的编程范式
说说我的理解,我认为编程范式**反应了某种编程语言的设计者希望程序员在使用他设计的语言时,用什么样的方式去思考问题。**因此不同的编程范式各有自身的长处,也难免有不足之处,在应对各种问题的时候,某种编程范式可能会更适合一些,选择得当,程序员的工作量会减少很多。
一种语言,可能支持多种编程范式,例如Swift,既支持面向协议编程,又支持面向对象编程、面向协议编程和函数式编程。只是在解决不同任务时,某些范式更合适。
Swift & 面向对象编程
举个例子,由于历史原因,在开发Cocoa和cocoa touch程序时,还是广泛使用了UIkit、Foundation框架和控制器视图(ViewController),他们使用的是OC时代的面向对象编程的方式,从继承的角度去考虑问题的。
UIkit框架类组织架构图
Swift & 面向过程编程
在解决动画、渲染、数据可视化等问题时,还是以古老的面向过程式的思维方式进行编程。因为在画图时,先绘制的东西会被后绘制的东西盖住,形成**层(Layer)**的概念。如果不能保证代码是同步一条条执行,那很可能每次渲染出来的图形是不同的。
Swift & 函数式编程
函数式编程本身是一个很大的课题,精髓是避免使用程序状态和可变对象,从而降低程序复杂度。函数式编程强调执行的结果,而非执行的过程。我们先构建一系列简单却具有一定功能的小函数,然后再将这些函数进行组装以实现完整的逻辑和复杂的运算,这是函数式编程的基本思想。
Swift在函数式编程方面表现虽不如Haskell来的纯粹,但是作为一个比Haskell流行的多的语言,和很多已有的函数式编程语言,Swift在语法上更加优雅灵活,语言本身也遵循了函数式的设计模式,是大部分程序员接触函数式编程的第一门语言。
面向协议编程出现的历史
回到今天的主角,面向协议编程。面向协议编程是由面向对象编程演变来的,协议在诸如c++和java等面向对象的语言中有一个别名,接口。(别骂街、别关网页,好戏在后头)
面向对象编程的好处
面向对象编程这些年能够风光无限,主要是由于它有的很多优点,例如:
- 数据封装
- 访问控制
- 类型抽象为类
- 继承关系,更符合人类思维
- 代码以逻辑关系组织到一起,方便阅读
- 由于继承、多态的特性,自然设计出高内聚、低耦合的系统结构,使得系统更灵活、更容易扩展,而且成本较低
- 在设计时,可重用现有的,在以前的项目的领域中已被测试过的类使系统满足业务需求并具有较高的质量
这么多优点,不可能一下全抛弃,所以也注定了面向协议编程不是一种革命性的编程范式,而是对面向对象编程的改良和演变。
Who is Crusty at Apple
长久以来,大家似乎默认了面向对象编程的好处都是由class带来的,但在苹果公司内部,有个叫Crusty的老兄think different了,他认为这一切是抽象类型带来的,而不是class带来的。我们知道,在面向对象编程中,接口和类都是对数据类型的抽取,类只是抽象类型众多实现的手段之一。在很多编程语言中,Struct和枚举同样可以做到对数据类型的抽取。
It's Type, not Classes. ————Crusty
Crusty认为,较好的抽象类型应该:
- 更多地支持值类型,同时也支持引用类型
- 更多地支持静态类型关联(编译期),同时也支持动态派发(runtime)
- 结构不庞大不复杂
- 模型可扩展
- 不给模型强制添加数据
- 不给模型增加初始化任务的负担
- 清楚哪些方法该实现哪些方法不需实现
经过改良的接口,达到了上面的要求,并改名为协议。至此,苹果决定将面向对象中的继承父类发展为服从协议,面向协议编程出现了。
面向协议编程的好处
前面提到了,经过Apple改良的接口达到了上节提出的较好抽象类型的目标。Swift的面向协议编程相比于OC的面向对象编程的好处主要体现在两点:1.动态派发的安全性、2.横切关注点。
动态派发的安全性
OC有强大的Runtime,在OC中,message与方法是在执行阶段绑定的,而不是编译阶段。简单的说 [a someFunc] 这样一个调用,在编译阶段,编译器并不知道someFunc要执行哪段代码。这个时候[a someFunc]会被转换为 objc_msgSend(a, "someFunc"),字面的意思也很容易理解,就是给a这个instance,发“someFunc”这个消息,以selector的形式。在运行阶段,执行到上述的objc_msgSend这个函数时。函数内部会到a对应的内存地址,寻找someFunc这个方法的地址,并执行。如果找不到,就会抛一个“unknown selector sent to instance”的异常。(比如.h中声明了方法,但.m中没有实现,就可以重现这个错误)
下面举的例子来自于喵神在MDCC 16上的演讲《面向协议编程与 Cocoa 的邂逅》中的ppt:
ViewController *v1 = ...
[v1 myMethod];
AnotherViewController *v2 = ...
[v2 myMethod];
NSObject *v3 = [NSObject new] // v3 ���� `myMethod`
NSArray *array = @[v1, v2, v3];
for (id obj in array) {
[obj myMethod];
}
// Runtime error:
// unrecognized selector sent to instance blabla
复制代码
上面的代码是可以编译过的,但是在运行时程序会崩溃。有了协议(protocol),可以申明数组的每个对象都是遵从某个协议的,如果塞进去了不遵从该协议的对象,就会报错,通不过编译。
protocol Greetable {
var name: String { get }
func greet()
}
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()
}
struct Bug: Greetable {
let name: String
}
// Compiler Error:
// 'Bug' does not conform to protocol 'Greetable'
// protocol requires function 'greet()'
复制代码
横切关注点(Cross-Cutting Concerns)
由于大部分面向对象的编程语言都是单继承的,导致了某些功能是不同类之间都需要的,但是由于改类已经继承了其他类,或者无法将不同类之间抽取出更多共性(或者说对于某一个小功能点来这样做代价太大),成为他们的父类,这样不得不在每个类里面重复一遍代码,使得代码很冗长。这样的小功能就可以称为横切关注点。
还是直接拿喵神的例子,假设我们有一个 ViewController,它继承自UIViewController,我们向其中添加一个 myMethod,如果这时候我们又有一个继承自 UITableViewController 的 AnotherViewController,我们也想向其中添加同样的 myMethod,这时,我们迎来了 OOP 的一大困境,那就是我们很难在不同继承关系的类里共用代码。这里的问题用“行话”来说叫做“横切关注点” (Cross-Cutting Concerns)。我们的关注点 myMethod 位于两条继承链 (UIViewController -> ViewCotroller 和 UIViewController -> UITableViewController -> AnotherViewController) 的横切面上。面向对象是一种不错的抽象方式,但是肯定不是最好的方式。它无法描述两个不同事物具有某个相同特性这一点。在这里,特性的组合要比继承更贴切事物的本质。
在swift中很容易抽取出协议,并用extension关键字提供协议的默认实现,从而避免的代码的重复。
参考
1.《面向协议编程与 Cocoa 的邂逅》
2.wiki:编程范式
3.A Brief Intro to Functional Programming
4.Objective-C 的消息机制如何理解