编程思想-面向协议

[toc]

一、产生背景

1.1 传统面向对象编程(OOP)的弊端/继承的痛点:

继承的痛点:滥用继承的话,后面如果要抽离功能就会牵一发而动全身。即抽离一个功能会导致也要把继承链上涉及的功能都抽出来。显然太麻烦了。

痛点的解决:使用面向协议,将每个单独功能一个协议,复杂功能将协议组合起来即可。如此后续抽离时,抽单独协议和实现类即可(swift中可以将实现写在扩展中,可能不涉及实现类)。

我们将网络库CJNetwork使用面向协议编程后

1、定义协议CQNetworkRequestCompletionClientProtocal.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NS_ASSUME_NONNULL_BEGIN

@protocol CQNetworkRequestCompletionClientProtocal <NSObject>

@required
/*
* 请求
*
* @param model 请求相关的信息(包含请求方法、请求地址、请求参数等)real\simulate\local
* @param completeBlock 请求结束的回调(为方便接口的重复利用回调中的responseModel使用id类型)
*
* @return 执行请求的任务
*/
- (NSURLSessionDataTask *)requestModel:(__kindof NSObject<CJRequestModelProtocol> *)model
completeBlock:(void (^)(CJResponeFailureType failureType, CJResponseModel *responseModel))completeBlock;
@end

NS_ASSUME_NONNULL_END

2、使用CQNetworkRequestCompletionClientProtocal

1
2
3
4
5
6
7
NS_ASSUME_NONNULL_BEGIN

@interface CJNetworkClient (Completion) <CQNetworkRequestCompletionClientProtocal>

@end

NS_ASSUME_NONNULL_END

1.2 什么是面向协议编程(POP)?

这个问题,我感觉比较明确的定义就是2015年Apple WWDC中说的一句话很直了的解释了:

1
2
Don't start with a class.
Start with a protocol.

即在程序设计中,不要以一个类开始设计,应该从一个协议开始,应抛弃之前OOP的对象设计理念,设计协议,这样不同的继承链之间也可以使用同一个协议。可以将协议看做一个组件,哪里需要哪里继承协议即可,而且协议是可以多继承的,iOS中的类只能单继承,这也是面向协议相对面向对象的一大优势。

1.3 Objective-C 和Swift的面向协议编程区别

OC和swift面向协议编程一个最大区别是OC的 Protocol 没有默认的实现,需要依赖具体的实现类实现协议定义的方法,而Swift2.0开始提供了Protocol + Extension,协议可以再 Extension中提供默认的实现,这样上层调用可以直接调用协议的默认实现。

严谨来说,OC不是一门面向协议编程的语言,因为 Protocol 只提供定义,而不提供实现,所以叫他 面向接口编程 更合适一些。

面向协议和依赖注入

依赖注入要求我们传入的参数遵循协议,这样做的好处是:

  • 我们可以在外部注入一个通用类,只要它遵循协议。
  • 也可以注入一个可测试的模拟类,便于测试。

参考文章:iOS-面向协议编程(POP)

二、替代继承的方式

参考文章:

1、协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> // 在屏幕中间弹出
> @protocol ShowInCenterProtocol <NSObject>
> - (void)show;
> @end
>
> // 在屏幕底部弹出
> @protocol ShowInBottomProtocol <NSObject>
> - (void)show;
> @end
>
> @class NormalVoicePlayer : NSObject <ShowInCenterProtocol>
>
> @end
>
>

2、用组合替代继承

eg1:想要为一个视图做一个show操作。(设想这个show方法后面可能随时会变,比如show的位置,或者动画)

AlertUtil:使用由AlertView/PopupView+PopupAction来组合,而不使用继承AlertView+为其添加show方法。

eg2:常见的举例还有,文本框TextField的根据历史记录填充、根据@邮箱填充。

3、类别category

1
2
3
@interface NSArray (OBJExtras)
- (void)removingFirstObject;
@end

三、使用面向协议为视图添加抖动功能

举例:为UIImageView和UIButton等添加视图的抖动

1、方法1:添加Category方式

如果你写过 Objective-C, 你很可能会把 shake() 写到一个 UIView 的分类(Category) 中(也就是 Swift 中的拓展 (extension)):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//  UIViewExtension.swift

import UIKit

extension UIView {

func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}

2、方法2:面向协议方式

但在swift里,我们除了对UIView添加extension外,其实还有另一种更好的方式:创建面向协议的视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class FoodImageView: UIImageView, Shakeable {
// 其他自定义写在这儿
}

class ActionButton: UIButton, Shakeable {
// 其他自定义写在这儿
}

class ViewController: UIViewController {

@IBOutlet weak var foodImageView: FoodImageView!
@IBOutlet weak var actionButton: ActionButton!

@IBAction func onShakeButtonTap(sender: AnyObject) {
foodImageView.shake()
actionButton.shake()
}
}

附上述面向协议的视图里所对的Shakeable 协议如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//  Shakeable.swift

import UIKit

protocol Shakeable { }

// 你可以只为 UIView 添加 shake 方法!
extension Shakeable where Self: UIView {

// shake 方法的默认实现
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}

参考文章:

四、使用面向协议处理reuseIdentifier

tableView的cell注册的基本代码如下

1
2
let foodCellNib = UINib(NibName: "FoodTableViewCell", bundle: nil)
tableView.registerNib(foodCellNib, forCellReuseIdentifier: "FoodTableViewCell")

改进1:FoodTableViewCell字符串改成由类获得

1
2
let foodCellNib = UINib(NibName: String(FoodTableViewCell), bundle: nil)
tableView.registerNib(foodCellNib, forCellReuseIdentifier: String(FoodTableViewCell))

改进2:

1
2
3
4
5
6
7
8
9
10
11
12
13
protocol ReusableView: class {}
// 你可以只为 UIView 添加 ReusableView 方法!
extension ReusableView where Self: UIView {
static var reuseIdentifier: String {
return String(self)
}
}


extension UITableViewCell: ReusableView { }

let foodCellNib = UINib(NibName: “FoodTableViewCell”, bundle: nil)
tableView.registerNib(foodCellNib, forCellReuseIdentifier:FoodTableViewCell.reuseIdentifier)

END