Protocol 的初探学习
POP
面向协议编程, POP(Protocol Orientation programing).
旨在提供一种 code 的架构方案, 其中解决的一个问题是在不同的类型之间共享代码.避免为了共享代码而不断的抽取父类, 最终形成一个很大的父类….
比如 JSONAble 的类型, 使用协议的话能解决之前我的一个问题:
返回的数据是一个 annotation 类型的, 可以通过 JSONAbleType 转换json, 但是这个 Annotation 类型又必须是BMAnnotation 类型, 这样才能将 [annotation] 传递给 Map 显示出来,
而 JSONAble 又是一个想保证其子类都有一个转换 json 的方法, 这种情况下使用协议我觉得就很好. 声明骨架, 传递这个类中该有的最基本的信息(类似就是 OC 中的了).
而且 Protocol 可以声明一个 init 构造方法, 这样不管哪个类遵守, 都需要有这个 init, Protocol 声明这个 init, 就不需要指定类型来创建一个类了.
起码在这个层面上来说, 挺方便, 即不用在遵守的类写上 final(这样就没有子类了, 不是个很道德的方法….), 遵守的类依然可以通过继承让其子类共用自己的方法.
通过 Protocol 声明的.init 方法在实现时需要 require 关键字(对于 struct 不需要 require 关键字)
在不同类之间共享代码
其实这也是一种解决多继承的方法, 在 Java8中好像也有类似的东西, 都是 interface + implement
像一些操作类型的, 工具类型的代码, 或者一些不同类之间的共性, 可以抽成协议类型. 举个栗子, 对地图的操作, 不一定是 MapViewController 需要, 虽然可以继承来实现共用代码, 但是一些不应该是 mapView 的类, 也需要能有操作地图的基本方法时, 就发生了该继承谁的冲突, 用继承的方法的话旧会有为了使用地图的方法就生成了一个不伦不类的class 出来, 逻辑上会很奇怪.
抽象点说比如飞行这个技能的基本属性和方式, 不一定是生物才有, 飞机也有. 有时候是同一个父类的东西, 不一定都会需要有同一个方法, 例如鸡也算是鸟类, 但不会飞. 这时将”飞”这个技能定义成一个协议的话, 就能灵活的确定那个 class 拥有飞行的方法, 这样旧能在不同的类中使用相同的方法或者有同一个声明, 在传递参数类型时可以使用这些方法, 也避免了因为继承而去拥有不该有的方法.
这里有一个情况: 一个类遵守了两个 Protocol, 其中都对一个属性进行了默认实现, 这时这个类就不能使用默认的 extension 中的实现了, 需要自己实现这个属性或者方法
在 Protocol’s extension 中使用 self
因为不知道谁遵守的 Protocol, 里面有什么, 所以字啊 extension 中使用 self 就会产生一些不明确, 要想使用的话, 需要加一些限制条件, 或者就取值操作而言, 可以直接使用 self
限制 Protocol 只能被 class 遵守
protocol name: class {
}
声明这个 class 的 Protocol, 使用这个协议的才可以声明为 weak 类型
限制只能被某个 class 遵守
protocol name where Self: UIViewController {
}
其并不是一个银弹(“银弹”是指一项可使软件工程的生产力在十年内提高十倍的技术或方法), 即不是在各方面各场合都是最合适的
参考
这篇文介绍了Swift 中 Protocol 滥用的情况, 认为很多时候有更简单的方法, 没必要一定使用 Protocol
在 Swift 的 Protocol 出现 extension 的使用之后, 很多人都会选择首先考虑通过能不能通过 Protocol 的方式完成任务, 我一开始使用的时候也是这样, 而且也碰到了不少坑…
“在Swift语言中,面向协议编程很流行。在“面向协议”那儿有很多Swift代码,一些开源库甚至将其声明为功能。我觉得协议在Swift中被过度滥用了,其实问题常常可以用更简单的方式来解决。简而言之,就是不要生搬硬套协议的条条框框,而不知变通。”
我觉得这句说的不错, 适用是分场景的, 合适的场景用合适的方案, 效率才会高点
但是在适合使用 Protocol 的情况下使用, 确实是一种比使用类更好的方式
有时候类的解决的好的, 协议不一定适用, 比如协议的 extension 中写了默认的最基本的实现, 但是类中要有特定的实现(其中包括基本实现)时, 不能对 Protocol 的 extension 进行 override, 这样那部分基础的function 就没有了复用的功能.
最后, 起码在传递类型时, Protocol 是好用的, 这和 OC 的 Protocol 作用又是一样的了.
所以, Swift 中 Protocol 的适用范围也是有限的, 在合适的条件下使用才能发挥他的便利性, 不然也只是瞎忙而已…
我觉得目前 Protocol 适用的地方有几个,
- 像OC 一样作为类的协议进行弱引用
- 构建一个框架进行传递(好像就是1.所做的)
- 共用相同的代码(而这个和全局函数作用又一样)
- 需要共用代码, 又需要传递这个类拥有这个代码的信息
- 定义相同行为的时候而这些行为又是完全相同的(即相对于1是有个 extension 的默认实现)
- 好像现在对于 Protocol 的观点也是和我一样, 一片混乱…有人说这么做很好…有人说这么做挺不好…两种文章读下去, 感觉他们说的都挺有道理, 然后我也陷入的混乱.
- 最后的总结就是…当类继承会不满足需求时, 考虑使用协议来声明方法甚至默认实现来在调用函数时传递这么个类型来调用某个方法.
code
我遇到的一些问题和注意事项:
不要字啊 extension 中 override 方法, 需要 override 的方法也不要写在 extension 中….这样…可以避免很多莫名其妙的问题…
这个在 Protocol 中也奏效. 当Protocol 中只在 extension 中写了 function 的话, 遵守的类 override 后, 调用这些 func 时会直接调用到 Protocol extension 中的实现
顺便说下, 结构体的值传递方式在对 cell bean 的传递方式感觉不是很友好, 或者是我没有掌握号结构体的使用方式, 毕竟现在还是用的挺少的
- 类遵守多个Protocol: 用逗号分割
- 属性遵守多个Protocol: 用 & 连接
对于协议中的可选方法, 在调用的时候未必会有实现, 在OC中可以用 [delegate responseto:(方法)]来判断是否有实现, Swift中是用?来使用可选链. :
delegate?.function?(实参)
对于仅限于类遵守的协议
有时候定义某个Protocol时, 设计上只要class来遵守, 其他的不予遵守, 这时候可以在声明Protocol的时候, 加上”class”的限定关键字, 且需要在最前端:
1 | protocol fly: class { |
协议组合
一个类遵守多个协议时, 用法:
1 | func wishHappyBirthday(to celebrator: Named & Aged) { |
要求某个传入的参数遵守多个协议
只需要将要求遵守的协议用”&”连接起来即可
可选协议的声明
你可以给协议定义可选要求,这些要求不需要强制遵循协议的类型实现。可选要求使用 optional 修饰符作为前缀放在协议的定义中。可选要求允许你的代码与 Objective-C 操作。协议和可选要求必须使用 @objc 标志标记。注意 @objc 协议只能被继承自 Objective-C 类或其他 @objc 类采纳。它们不能被结构体或者枚举采纳。
1 | @objc protocol CounterDataSource { |
更新
看到这篇文章, 可以说是对协议的具体使用的很好的范例有了一个认识, 里面介绍了在我们开发中常见的场景中使用 POP
来提高代码的效率和可读性.
(以下记录仅仅是简单的概括这片文章, 达到自己能看到这些, 就能对应的想到原文描述的东西, 记关键节点,不记叶子)
实践 POP
Views
总有一些需求, 在 A View 需要一些功能啦, 过一段时间, B View 也需要这个功能了, 肿么办… 复制粘贴? (拿到砍啊! 放个二维码, 打款来再说…)
好吧, 当你意识到不会就这么2个东西需要这样的时候, 就应该想着抽象出来了. 那么, 这个功能放在哪里就需要想想了~ 不能如果对这个功能有修改, 就要修改几个地方?
放在 super view
的分类或者扩展中? 嗯, 不错~ 一直以来都是这么干的, 但是这样会有个结构上的问题. 这个 super view
会越来越臃肿… 而且其他继承自这个 super view
也会有这些功能了, 不管是不是逻辑上会需要…
协议的出现
protocol 么在OC
就有了啊, 但是为什么现在才能更好的解决这个问题咧?
在 Swift 的协议刚出来的时候, 他的扩展也很弱, 没什么卵用. 后来, 大招就来了, 可以在协议扩展里添加一个默认实现的方法, 遵守协议的如果没有自己实现, 会使用声明这个扩展时做的默认实现~
这样我们就可以从父类中逃脱, 不用老是什么都加到父类去了~
例子: (采用原文)
1 | // Shakeable.swift |
在协议扩展的帮组下, 你可以限制再一个特定的类里, 这样, 又做了一层控制, 并不是每个遵守这个协议的都有这个方法, 遵守协议的特定的类才有, 如例子中的: extension Shakeable where Self: UIView
, 这个只有是UIView
的才可以有这个默认实现.
在需要这个功能的View
中, 我们只需要:
1 | class FoodImageView: UIImageView, Shakeable, Dimmable { |
当你不需要这个View
有某个功能时, 仅仅做的是不去遵守对应的协议
1 | class FoodImageView: UIImageView, Dimmable { |
通过协议, 我们可以很容易的获得乐高似的结构, 如果想学习使用协议更强大的使用方式的话, 可以看看这篇文章, 试试创建一个有过度效果的可调暗视图.
(UITable)ViewController
TableView/CollectionView
Controller 算是在 iOS 中接触的最频繁的两个类了, 看看这里是怎么在使用这些的时候通过协议来提高效率的, 这里提到的方法很斯巴拉西! 其中还涉及到了泛型的使用, 从 OC
转来的, 想必这些例子是十分有用了, 我是这么想的 :)
使用一个tableview
去展示一些事物, 这是我们经常做的事情, 在之前, 在 controller 配置通用的做法是(): 用不同的 Nib/类名 & ReuseIdentifier 注册一个 Cell 的类
-> 在代理中根据ReuseIdentifier
获取这个类, 然后使用
1 | // FoodLaLaViewController |
so, 用 String() 对于直接使用”字符串”已经很好了, 但是, 在Protocol
面前, 这样就够了吗? Hell No!
我们需要更懒的方法!
so, here is the code
1 | // 首先来个协议, 表示这个类是一个 可重用的东西 |
还有 Nid 加载的, 也可以用 protocol 来做
1 | protocol NibLoadableView: class { } |
最后, 我们用起来会是像这样,
1 | override func viewDidLoad() { |
对 cell 是这样了, 我们还需要更懒一点, 就想给注册这个 cell 而已, 还要拿 nib, 还要拿 reuseIdentifier 也太麻烦了把, 这样~
1 | extension UITableView { |
我们给 UITableView 也搞一个扩展(这里不用协议哦, 因为就 UITableView 这个类扩展了这个方法, 毕竟只有这个类是这么用的)
然后我们给tableView
注册一个 Cell, 只需要这样就好了 只需要一行代码, 就完成了注册一个 cell, 不需要任何字符串1
tableView.register(FoodTableViewCell)
另外, 在使用的时候, 我们也需要dequeue
某个 cell 来使用, 能不能也简单点咧? 而且具有通用性? of course!
1 | extension UITableView { |
这样, 就可以从之前的代码形式:
1 | guard let cell = tableView.dequeueReusableCellWithIdentifier(“FoodTableViewCell", forIndexPath: indexPath) |
换成现在这样的:
1 | let cell = tableView.dequeueReusableCell(forIndexPath: indexPath) as FoodTableViewCell |
甚至不同类的 cell
1 | if indexPath.row == 0 { |
iOS Cell 注册 & 用 Swift 协议扩展和泛型来实现复用
这部分源自 Guille Gonzalez,他把这个原则���用到 collection view 上,你也可以把这个方法运用到其他你有问题的 UIKit 的组件上,例如 Swift 中面向协议的 Segue 标识符。你可以在每天的编程中都像那样使用协议,这样也会安全些。它也是源自 Apple 去年 WWDC 上的例子。面向协议编程真的很棒。
更新2018-09-22 14:06:35: 看到一个库, 有博客, 还有 SwiftGG 的翻译:
网络
我们要怎么使用协议来优化我们在网络调用时候的写法?
比如我们说, 我们有一个 Service, 里面有get
函数来发起请求
1 | struct FoodService { |
现在我们再VeiwController
中处理这个行为
1 | // FoodLaLaViewController |
这是一个典型的调用 API 的模式。但是整个 view controller 依赖上食物数组的加载了:如果没有数据或者数据错误,它会失败。确认 view controller 是按预期正确处理了数据的最好方式是……测试。
测试么…在这里测试是很痛苦的, 有了各种东西, 我们都部署很可控, 所以, 需要改变一些结构和方法, 让我们的测试能顺利点进行.
首先是, 这个Service
为什么是自爱 getFood
方法里创建的, 外部对这个Service
不具有可控性, 不好测试, 我们需要对这个FoodService
又更多的控制, 需要把getFood
改成这样, 才能在测试时注入我们能控制的东西
1 | // FoodLaLaViewController |
那我们怎么提供一个测试的Service
?
1 | struct FoodService { |
这是一个值类型, 也就是你不能创建他的子类, 反而, 你应该使用协议, 你可以想像, 每个Service
都会有 get
, 都有类似这种网络的请求方法, 我们需要一个协议把它抽象出来
1 | protocol Gettable { |
我们可以使用协议和泛型, 来降调用的类, 返回的数据进行通用化, 我们说每一个遵循 Gettable 协议的地方都有 get 函数,而且它接收一个完成句柄和这个类型的结果。在我们的例子中,这会是 food (但是在 dessert 服务中,它会是 dessert;这是能互相交换的)。
1 | struct FoodService: Gettable { |
回到 food 服务,唯一的改变就是它需要遵循 Gettable 协议。 get 函数已经实现了。它只需要接收一个 completionHandler,这个句柄接收结果的条目……因为相关类型的协议是智能的 (结果是 food 数组,相关类型就是 food 数组)。你不需要描述它。
回到 view controller,这基本上就是一样的了。
1 | // FoodLaLaViewController |
在测试上, 我们穿件一个 Fake_FoodService
, 它有两个事情: 1) 遵循 Gettable
协议,2) 相关类型需要是 food
数组。
1 | // FoodLaLaViewControllerTests |
这个测试我们是让他通过了, 但是你也可以返回一个失败的结果来完成 ViewController
的测试
1 | // FoodLaLaViewControllerTests |
POP 实践!结论
我们讨论了协议是如何在实际工作中运用的,特别是在每天编码过程中是如何把它使用到视图控制器,视图和网络中去的。本篇演讲帮助你编写出安全的,可维护的,可重用的,更统一的,模块化代码。更加易于测试的代码。相比于子类而言,协议更棒。
然而,协议也会被滥用。我可能使用了过多的协议了:我学习它,它是个全新的东西而且吸引眼球,我希望无时不刻都使用它……但是这是不必要的。在我的第一个例子里面,当我有一个抖动的视图和抖动函数的时候,这就很棒。只有在两个视图都需要抽象的时候,我需要重构它们的时候,放到协议里才是合理的。不要疯狂地使用协议
以上是原文的结论, 在我的结论看来, 这片文章不仅讲了使用协议的新的, 还讲了和泛型一起使用的例子, 对原本泛型不太了解的我起到了很大的作用.
而且里面推荐的文章也很值得一看, 提高自己使用的水平