“我们如何在每天的开发过程中使用面向协议编程?Natasha 回答了这个问题,并专门针对 POP 的实际应用开发给出了解决方案,包含视图,视图控制器和网络的实例。关注本篇在App Builders CH大会上的演讲,你将从面向对象编程转向面向协议编程,这样能使你的 Swift 编程更加清晰、更加易读!
回到现实 – 我们假设 Swift 是最棒的编程语言。
今天,我将谈谈基于 Swift 的面向协议编程,我会侧重在如何实现上。让我们称它POP。
基于 Swift 的面向协议编程(00:37)
当 Swift 刚刚出现的时候,学习新东西都是令人兴奋的。第一年,我很高兴能学习它,我之前在 Swift 里面使用我的 Objective C 代码 (有的时候用些值类型和更加有趣的东西)。但是直到去年的 WWDC,协议扩展出现了。
Dave Abrahams (让你大开眼界的教授) 做了一次令人大开眼界的演讲“基于 Swift 的面向协议编程”。他声称 “Swift 就是一个面向协议的编程语言。” 如果你看看 Swift 的标准库,那有超过 50 个协议。这就是这门语言的成形之处,它使用了许多的协议而且这也是我们想借鉴的地方。Dave 还给了一个如何使用协议来改进我们现有代码的例子。他使用了 drawables 的例子,比如正方形、三角形,圆形。使用协议能够让它们的实现变得特别令人吃惊。我是被震撼到了,但是对于我来说我却无法直接使用,因为我在每天的工作中不使用 drawables。
回去以后,我冥思苦想,我该如何在每天的程序中使用面向协议编程呢。我们都有些从 Objective-C 和其他编程语言继承下来的编程模式,所以从面向对象转变到面向协议是一件很难的事情。
实践 POP!(03:05)
过去一年,我终于有机会实验一下使用协议,我想分享些我改进代码的例子。因为这是实践面向协议编程,我将会讲到View
、(UITable)ViewController
和Networking
。希望这能帮助你们考虑如何在你们的实际工作中使用协议。
Views(03:24)
让我们假设你的产品经理过来和你说,“我们���在点击那个按钮时候出现一个视图,而且它会抖动。” 这是一个非常常见的动画,比如,在你的密码输入框上 – 当用户输入���错误密码时,它就会抖动。
我们常常都是从 Stack Overflow 开始的(笑)。一些人可能已经有了 Swift 抖动对象的基础代码。一些人甚至都有 Swift 的抖动对象的代码,我想都不用想,只要稍稍修改一下。最难的部分当然是架构:我在哪里集成这些代码呢?
// FoodImageView.swift
import UIKit
class FoodImageView: UIImageView {
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
repeatCount = 5
autoreverses = true
fromValue NSValue(CGPointCGPointMake(selfcenterx - 4.0, y))
toValue + layer.addAnimation(animationforKey)
}
}
我将创建一个UIImageView
的子类,创建我的FoodImageView
然后增加一个抖动的动画:
FoodImageView
的子类,我有一个 shake 函数,然后完成了!。10 分钟我就完成了这个功能。我很开心,我的代码工作得很正常。
然后,你的产品经理过来说,”你需要在抖动视图的时候抖动按钮。” 然后我回去对按钮做了同样的事情。
@H_404_167@// ShakeableButton.swift ActionButtonUIButton }子类,创建一个按钮,增加一个shake()
函数,和我的ViewController
。现在我能抖动我的 food 图像视图和按钮了,完成了。
UIView
的类别,在 Swift 里面,这就是扩展。
我能这样做,因为UIButton
和UIImageView
都是 UI 视图。我能扩展 UI 视图而且增加一个 shake 函数。现在我仍然可以给我的按钮和图像视图都加上其他的逻辑,但是 shake 函数就到处都是了。
foodImageView
和actionButton
来说,你看不出来任何抖动的意图。整个类里面没有任何东西能告诉你它需要抖动。这不清楚,是因为别处会随机存在一个抖动函数。
如果你常常为类别和 UI view 的扩展这样做的话,你可能会有更好的办法。这就是所谓的科学怪人的垃圾地点,你增加了一个 shake 函数然后有人来和你说, “我想要一个可调暗的视图”。然后你增加一个 dim 函数和其他别处随机的调用函数。这样,文件会变得越来越长,不可读,很难找到垃圾,因为这些随机调用的事情都可以在 UI 视图里面完成,尽管有些时候也许只有一两个地方需要这么做。
意图是什么并不清晰。我们如何改变这点呢?
这是一次面向协议编程的演讲,我们当然会用到协议。让我们创建一个Shakeable
的协议:
你仍然可以使用你原来想用的同样强大的扩展功能,但是你有协议了。任何遵循协议的非视图不会工作。只有视图才能有这个 shake 的默认实现。
FoodImageView
和ActionButton
会遵循Shakeable
协议。它们会有 shake 函数,现在的可读性强多了 –- 我可以理解 shaking 是有意存在的。如果你在别处使用视图,我需要想想,”在这也需要抖动吗?”。它增强了可读性,但是代码还是闭合的和可重用的。
假设我们想抖动和调暗视图。我们会有另外一个协议,一个Dimmable
协议,然后我们可以为了调暗做一个协议扩展。再强调一遍,通过看类的定义来知晓这个类的用途,这样,意图就会很明显了。
Shakeable
协议就好了。
现在我们高兴了,可以去吃 Pop-tarts 了。
(UITable)ViewControllers(10:09)
这是一个应用,Instagram 食物:它给你展示不同地点的美食照片。
@H_404_167@// FoodLaLaViewController override viewDidLoad{ super() foodCellNib UINibNibName"FoodTableViewCell"bundle: nil) tableViewregisterNibfoodCellNibforCellReuseIdentifier) 这是一个tableView
。这是我们一直都会编写的基础代码。当视图加载的时候,我们会从Nib
中加载 cell;我们定制NibName
,然后我们使用一个 ReuseIdentifier 来注册Nib
。
不幸的是,因为 UIKit 创建方式的限制,我们不得不使用字符串。我喜欢为我的 cell 使用相同的 identifiers 来作为 cell 的名字。
我们立刻就能看到低效的地方。如果你以前使用的是 Objective-C,我常常使用NSString
作为类。在 Swift 里面,你可以使用String
(稍好一点),相较 Objective-C 而言,我们已经足够好了。我们常常就使用String
,但是如果一个没有做过 iOS 开发的实习生来到我们的项目,这个函数对他来说就是天书。你会随机的字符串化一些名字,”为什么你这样做呢?”。同时,如果你不在 storyboard 里指定 identifier 的话,现在它会 crashing 而且他们还不知道什么原因。我们该如何改进呢?
因为我们不再使用 Objective-C 了,我们可以为这些 cell 重用视图协议。
UICollectionView
UITableView
Cell
也适用。这是我们的可复用的 identifier。我们可以把这个不得不用的讨厌逻辑封装起来. 因为UIKit
需要它。现在我们能扩展每一个单独的UITableViewCell
了。
我们可以对UICollectionViewCell
做同样的事情来扩展可复用的视图协议。每一个单独的 cell 都有一个默认的reuseIdentifier
,我们再不需要输入一遍或者担心了。我们说,FoodTableViewCell
、reuseIdentifier
,它将会通过字符串化类来帮助我们完成这件事情。
这依旧很长,但是更易读:
NibName
做同样的事情,因为我们不想处理字符串。我们能创建一个NibLoadableView
(任何能从Nib
里加载的类)。我们会有一个NibName
,而且它会返回类名的字符串版本。
如何从Nib
里面加载的视图,比如我们的TableViewCell
,将会遵循 Nib 可加载视图协议。它会自动地有一个NibName
的属性,而且会字符串化类名。至少实习生能明白我们现在有了 cell 的NibName
,它是这个 cell 的reuseIdentifier
,而且每一次我们注册这个类的时候,每一个TableViewCell
都是这样的。
我们现在能再进一步,使用泛型来注册我们的 cells,然后提取这两行代码。
tableView
然后创建一个注册类,这个类可以接收一个类型包含这两种协议。它有一个可重用的标识符和一个从那些协议里面获取的Nib
名字。现在我们可以完整地从遵循NibLoadableView
要求的Nib
名字的位置,抽取这两行代码的逻辑出来。我们知道它有一个叫做NibName
的属性,而且 cell 会遵循可重用的视图协议 (它们会有可重用的标识符属性)。这两行代码,本来我们需要在每一个单独的表格视图里面都输入一遍,现在被抽取出来了。只需要一行代码,我们就完成 cell 的注册,这看起来会干净许多。你不需要再处理字符串了。
我们可以更进一步。我们不得不注册 cells,我们也不得不清理 cells。我们可以用泛型和协议来代替这些本来很丑的代码:当你需要清理的时候,你需要指明reuseIdentifier
。在 Swift 里面,这只需要三行代码,因为我们有 optionals。
当你输入这行代码的时候,你都会觉得丑陋。它源自 Objective-C,我们从 UIKit 里面开始有它,我们对它没有太多的办法。但是使用协议,我们可以抽取这些丑陋的地方,因为我们对每一个单独的表格视图 cell 都有reuseIdentifier
。
forIndexPath
里清理 cell,而且我们说明 cell 是哪个。如果你有多个 cells,你可以把它转换成你注册的那个 cell,它马上就会知道它的类型是什么了。
这太神奇了!这是个替代原来我们在 Objective-C 里方式的好方法,这个方法混合了 Swift 和 optionals,并采用了协议和泛型,给我们的项目带来更好看的代码。
iOS Cell 注册 & 用 Swift 协议扩展和泛型来实现复用(17:28)
这部分源自Guille Gonzalez,他把这个原则���用到 collection view 上,你也可以把这个方法运用到其他你有问题的 UIKit 的组件上,例如Swift 中面向协议的 Segue 标识符。你可以在每天的编程中都像那样使用协议,这样也会安全些。它也是源自 Apple 去年 WWDC 上的例子。面向协议编程真的很棒。
网络(18:25)
使用网络的时候���你一般要调用 API。 我常常这样做:我有一些服务 (比如 我从服务器那获取食物),我有一个get
函数,它会调用 API 然后得到结果。我想使用 Swift 的错误处理,但是它是异步的,我不能抛出错误。
结果枚举很简单。当服务器返回结果的时候,我们能把它解析为成功然后返回一个食物条目的数组。如果失败了,我们能返回一个错误码,然后完成句柄中的 view controller 会知道如何处理这些情况。
dataSource
。当视图加载的时候,我们将调用异步 API,然后再完成句柄中得到结果。如果结果是一组食物,太棒了:我们重置数据,重新加载表格视图。如果结果是个错误,我们会给用户一个错误提示,然后处理它。
View Controller 测试?!!!(20:54)
View Controller 测试很痛苦。在这个例子中,因为我们有了服务,异步 API 调用,一个完成代码块,和一些结果枚举,测试就会更加痛苦。这些都使得测试 view controller 是否按预期工作变得更加困难。
getFood()
实例化一个 food 服务的时候,我们的测试没有机会能注入。第一个测试是增加依赖注入。
@H_404_167@// FoodLaLaViewController
fromService service{
servicein
// handle result
// FoodLaLaViewControllerTests
testFetchFood{
viewControllerfromService())
// now what?
现在我们的getFood()
函数接收FoodService
参数,这样我们就有了更多的控制权了,之后我们才能做更多的测试。我们有 controller,叫做getFood
函数,然后我们给它传入FoodService
。当然,我们想要对于FoodService
完整的控制。我们如何实现呢?
FoodService
有一个 get 函数,completionHandler
会给出结果。你可以想象你应用里面的每一个服务,每一个 API 调用都需要一个get
函数 (比如 dessert),也会有类似的东西。它有一个完成句柄能够接收结果,然后解析它。
我们马上能让它变得更通用:
Gettable
协议的地方都有 get 函数,而且它接收一个完成句柄和这个类型的结果。在我们的例子中,这会是 food (但是在 dessert 服务中,它会是 dessert;这是能互相交换的)。
Gettable
协议。 get 函数已经实现了。它只需要接收一个completionHandler
,这个句柄接收结果的条目……因为相关类型的协议是智能的 (结果是 food 数组,相关类型就是 food 数组)。你不需要描述它。
回到 view controller,这基本上就是一样的了。
getFood()
函数只能获得 food 条目的结果。否则,它就是其他遵循Gettable
协议的东西。这使得我们能对传入的,诸如FoodService
的参数有更强的控制 – 因为它不需要一定是FoodService
,我们能注入其他的东西。
在我的测试中,我们创建了一个Fake_FoodService
。它有两个事情: 1) 遵循Gettable
协议,2) 相关类型需要是 food 数组。
Gettable
,它接收 food 的结果,然后返回一个 food 数组。因为这是测试,我们想确定Gettable
的get
函数被调用了,因为它能返回,而且函数理论上可以分配一个从任意地方获取的 food 条目的数组。我们需要保证它被调用到;在这个例子里面是成功的例子,但是你能通过注入失败来完成同样的对 view controller 的测试,来保证你的 view controller 的行为在你的输入结果的条件下是正常的。测试如下:
fakeFoodService
:我们能注入我们的fakeFoodService
(这个我们有更强的控制力),而且我们能测试get
函数能被调用到,而且通过我们的FoodService
注入的数据源和我们赋给 view controller 的数据源是同组数据。通过增加Gettable
协议,我们有了一个对 view controller 的强大测试,我们有了对所有服务的测试框架。我们能实现一个可删除的,可更新的,可创建的协议;关于服务,你能马上看出哪个函数需要被实现而且容易注入,然后测试它们。
我用协议写了一个注入 storyboards 的例子,而且我强烈推荐 Alexis Gallagher 的这篇演讲相关类型的协议. 我简化了它,但是相关类型的协议也会常常出乎意料。使用它时,你可能会感到沮丧,这篇文章会使你平静些,因为他解释了它的限制。
然后,你可以回来,享受爆米花了。
POP 实践!结论(27:40)
我们讨论了协议是如何在实际工作中运用的,特别是在每天编码过程中是如何把它使用到视图控制器,视图和网络中去的。本篇演讲帮助你编写出安全的,可维护的,可重用的,更统一的,模块化代码。更加易于测试的代码。相比于子类而言,协议更棒。
然而,协议也会被滥用。我可能使用了过多的协议了:我学习它,它是个全新的东西而且吸引眼球,我希望无时不刻都使用它……但是这是不必要的。在我的第一个例子里面,当我有一个抖动的视图和抖动函数的时候,这就很棒。只有在两个视图都需要抽象的时候,我需要重构它们的时候,放到协议里才是合理的。不要疯狂地使用协议。
作为结束,两个非常有趣的演讲:
Beyond Crusty: 真实世界的协议作者:Rob Napier,基于真实世界的协议。他一开始介绍了些坏的代码,用协议重构了它们……使用了 10 个协议和 20 行代码。他通过结构解决了问题,仅仅用了四行代码。在 Swift 里,我们有不同的新的东西,包括强大的枚举,结构和协议。基于具体问题,可能需要不同的解决方案。我推荐用协议实验,但是还是要想想,”这个问题结构能行吗,或者组合就足够了?”。
Blending Cultures: 函数式,面向协议和面向对象编程的最佳实践作者: Daniel Steinberg。他从你如何使用每个方法开始,因为我们不受限于协议,我们仍然有面向对象的思想和函数式思想。这是一个很棒的演讲,它展示了如何使用一切手段来完成你的代码中不变部分和变化部分的抽取。
在你每天编程的时候,希望你能考虑使用协议!