什么是POP (Protocol-oriented-programing)
- 在我们平常工作中,经常使用的是OOP(Object-Oriented-Programing),比如C++、OC、JAVA。他们是把一些数据或动作集合当做一个对象来看待,比如树、人、机器等。POP则不同,它从另一个视角看待事物:属性。一个对象拥有的数据属性和动作属性,都可以成为一种协议(protocol),不同的对象间有着共同的协议或相似的协议。
- 实际编码过程中,定义各种协议来规范和拓展类的方式,统称叫做POP。
举个🌰
以前我们可能会这样构建一个类:
class bird {
var name = ""
var canFly = true
}
class penguin: bird {
override init() {
super.init()
self.canFly = false
self.name = "penguin"
}
}
对于OOP来说是这样定义的,bird为penguin的父类,子类继承和覆盖了父类的属性。 而在POP中,我们这么干:
protocol Flyable {
var canFly: Bool { get }
}
struct Plane: Flyable {
var canFly: Bool = true
}
struct Swallow: Flyable {
var canFly: Bool = true
}
明显Flyable属性其实不单独属于鸟类,它同样也是很多其他对象的属性。所以在构造其他对象时,我们可以给他们贴上同样的“标签”,接下来我们试着丰富一下我们的标签。 我们定义一个可以描述速度的协议:
protocol Racer {
var speed: Double { get }
}
extension Plane: Racer {
var speed: Double {
return 1000
}
}
extension Swallow: Racer {
var speed: Double {
return 200
}
}
这样,我们的✈️和🐦就拥有速度了~ 然后我们可以用Racer来进行拓展。 拓展一个很简单的功能,我们知道Array是继承Sequence协议的,我们可以在Sequence中加入一个功能,使得快速找出Sequence中速度最快的对象。
extension Sequence where Iterator.Element == Racer {
func fastest() -> Iterator.Element? {
return self.max(by: { (a: Iterator.Element, b: Iterator.Element) -> Bool in
return a.speed < b.speed
})
}
}
let plane = Plane()
let swallow = Swallow()
let array: Array<Racer> = [plane, swallow]
array.fastest()
这样Array就具有了一个简单的功能,我们可以利用这种思想去构建一个体系,Swift中的Array和Dictionary就是很好的例子。
Pop的简单应用
先来看一个简单的例子,iOS的TableView我们通常这样写:
class CommentCell: UITableViewCell {}
class MyViewcontroller: UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
let cellNib = UINib(nibName: "CommentCell", bundle: nil)
tableView .register(cellNib, forCellReuseIdentifier: "comment_cell")
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "comment_cell", for: indexPath) as? CommentCell else {
return UITableViewCell()
}
return cell;
}
}
这看起来有点复杂,夹杂着各种判断和转换。所以我们可以利用Swift的特性和POP方式:
protocol ReusableView {
}
extension ReusableView where Self: UITableViewCell {
static var reusedIdentifier: String {
return String(describing: self.self)
}
}
protocol NibLoadableView: class { }
extension NibLoadableView where Self: UIView {
static var NibName: String {
return String(describing: self.self)
}
}
extension CommentCell: ReusableView, NibLoadableView {
}
有了这两个协议,我们就可以对TableView的方法进行改造,使用泛型方法:
extension UITableView {
func registerNib<T: UITableViewCell>(_: T.Type) -> Void
where T: ReusableView, T: NibLoadableView {
let nib = UINib(nibName: T.NibName, bundle: nil)
self.register(nib, forCellReuseIdentifier: T.reusedIdentifier)
}
func dequeReusableCell<T: UITableViewCell>(forIndexPath ip: IndexPath) -> T
where T: ReusableView {
guard let cell = self.dequeueReusableCell(withIdentifier: T.reusedIdentifier, for: ip) as? T else {
fatalError("couldn't deque cell with identifier: \(T.reusedIdentifier)")
}
return cell
}
}
然后在实现的时候,我们会发现一切变得更好了
extension MyViewcontroller {
func register() -> Void {
tableView.registerNib(CommentCell.self)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) ->
UITableViewCell {
return tableView.dequeReusableCell(forIndexPath: indexPath) as CommentCell
}
}
这只是个实例,这种思想并不会一定帮助你的代码效率更高,但是合理利用下,会让你的代码可读性变得很强,拓展性变的极高,思路变得清晰。下面我们看看在实际项目中POP的应用。
一个网络层框架
这个是一个项目中实际使用的网络请求框架。在父类的APIManager
中包含了一个指向子类的指针,并且强制子类实现一个APIManagerProtocol
。
父类初始化时要求子类的约束条件。
- (instancetype)init {
if (self = [super init]) {
if ([self conformsToProtocol:@protocol(XLAPIManagerProtocol)]) {
self.child = (id <XLAPIManagerProtocol>)self;
self.shouldAutoRetry = [self.child respondsToSelector:@selector(autoRetryCountWithErrorCode:)];
self.autoProcessReceivedData = YES;
} else {
NSAssert(NO, @"APIManager 子类必须集成XLAPIManagerProtocl");
}
}
return self;
}
利用self.child来规范子类的实现,同时通过protocol来获取子类的相关信息,使功能实现聚合在BaseAPI中。比如这个获取完整URLString的方法:
- (NSString *)fullURLString {
if (!self.urlString) {
NSMutableString *url = [NSMutableString stringWithString:[self.child server].url];
if ([self.child apiVersion] && [self.child apiVersion].length > 0) {
[url appendFormat:@"/%@", [self.child apiVersion]];
}
[url appendFormat:@"/%@", [self.child apiName]];
self.urlString = url;
}
return self.urlString;
}
实现后我们会发现,子类实现API会非常清晰和简洁。一个API类的实现,只需要几行代码
@implementation KNBindInfoAPIManager
- (NSString *)apiName {
return @"bindinfo";
}
- (NSString *)apiVersion {
return @"";
}
- (NSString *)httpMethod {
return @"GET";
}
- (XLServer *)server {
return [XLVipExtServer sharedServer];
}
@end
在以前的错误码处理中,都是统一在APIManager中
处理。但是后来遇到一个问题,由于请求的服务器不同,导致错误码处理不一致,再看我们之前的实现,就有点代码的坏味道了。分析后发现,错误码其实是和服务器挂钩,真正该如何处理一个数据应该是由该服务器说了算,所以我们给XLServer
这个类加了一个协议:
@protocol XLServerDataProcessProtocol <NSObject>
/**
* 处理数据,可抛出异常或请求重试。
*
* @param data 要处理的数据
* @param error 错误
* @param retry 是否需要重试
*/
- (void)handleData:(id)data error:(NSError **)error shouldRetry:(BOOL *)retry;
// 在父APIManager.m 实现中
if ([self.child.server conformsToProtocol:@protocol(XLServerDataProcessProtocol)]) {
NSError *error = nil;
BOOL retry = NO;
[(id <XLServerDataProcessProtocol>)self.child.server handleData:responseObject
error:&error
shouldRetry:&retry]
}
在API处理结果中,对当前子类的server进行判断,如果有响应协议,则交给相应服务器对象处理。 这些在Objective-C中的实践例子,实际用处是规范了各个类的数据和动作的实现,使得代码结构变得清晰,逻辑严密。
简谈函数式编程
- 在Swift中,函数成为了First class value,使得我们可以传递、使用函数就像使用一个数字、一个字符串一样简单。
- 可以粗略的看待一个函数,就是一个过程。
- 我们可以通过变换函数,创造一个个“小工具”,这些工具可以帮助我们构建复杂的过程。
- 函数方便我们调试和找出bug
一个简单的函数式思想的例子
假如我们写一个战船的例子,我们需要一个方法来定位敌方战船是否在我们的有效射程范围内:
typealias Distance = Double
struct Position {
var x: Double
var y: Double
}
extension Position {
func inRange(range: Distance) -> Bool {
return sqrt(x * x + y * y) <= range
}
}
struct Ship {
var position: Position
var firingRange : Distance
var unsafeRange : Distance
}
extension Ship {
// 最初我们就判断是不是在射程范围里
func canEngageShip(target: Ship) -> Bool {
let dx = target.position.x - position.x
let dy = target.position.y - position.y
return sqrt(dx * dx + dy * dy) <= firingRange
}
// 发现同时,敌方也不能离我们太近
func canSafelyEngageShip(target: Ship) -> Bool {
let dx = target.position.x - position.x
let dy = target.position.y - position.y
let targetDistance = sqrt(dx * dx + dy * dy)
return targetDistance <= firingRange && targetDistance > unsafeRange
}
// 友方战船也不能离目标太近
func canSafelyEngageShip1(target: Ship, friendly: Ship) -> Bool {
let dx = target.position.x - position.x
let dy = target.position.y - position.y
let targetDistance = sqrt(dx * dx + dy * dy)
let friendlyDx = friendly.position.x - target.position.x
let friendlyDy = friendly.position.y - target.position.y
let friendlyDistance = sqrt(friendlyDx * friendlyDx +
friendlyDy * friendlyDy)
return targetDistance <= firingRange
&& targetDistance > unsafeRange && (friendlyDistance > friendly.unsafeRange)
}
}
为了简化计算,我们抽取其中的几个方法:
extension Position {
func minus(_ p: Position) -> Position {
return Position(x: x - p.x, y: y - p.y)
}
var length: Double {
return sqrt(x * x + y * y)
}
}
extension Ship {
func canSafelyEngageShip2(target: Ship, friendly: Ship) -> Bool {
let targetDistance = target.position.minus(self.position).length
let friendlyDistance = friendly.position.minus(target.position).length
return targetDistance <= firingRange
&& targetDistance > unsafeRange && (friendlyDistance > unsafeRange)
}
}
最终这个函数表现成这样,看起来条例清晰,逻辑清楚,但是难免在整个函数构建过程中发现,这个过程包含着一次次重构。
现在我们换个思路,把这一个关键的判断过程看做一个对象:
typealias Region = (Position) -> Bool
我们把这个过程定义成一个名为区域的类型。然后我们需要一些工具来生成或者改变区域:
func circle(_ radius: Distance) -> Region {
return { $0.length <= radius }
}
func shift(_ region: @escaping Region, offset: Position) -> Region {
return { region($0.minus(offset)) }
}
func invert(_ region: @escaping Region) -> Region {
return { !region($0) }
}
func intersection(_ region1: @escaping Region, _ region2: @escaping Region) -> Region {
return { point in
region1(point) && region2(point)
}
}
func union(_ region1: @escaping Region, _ region2: @escaping Region) -> Region {
return { point in region1(point) || region2(point) }
}
func difference(_ region1: @escaping Region, minus: @escaping Region) -> Region {
return intersection(region1, invert(minus))
}
有了这些小工具,我们可以轻松的定义之前的函数
extension Ship {
func canSafelyEngageShip(target: Ship, friendly: Ship) -> Bool {
// 生成自己的安全攻击范围
let rangeRegion = difference(circle(firingRange), minus: circle(unsafeRange))
// 位移一下到战船所处坐标
let firingRegion = shift(rangeRegion, offset: position)
// 生成友方的安全范围
let friendlyRegion = shift(circle(friendly.unsafeRange),offset: friendly.position)
// 生成实际有效范围
let resultRegion = difference(firingRegion, minus: friendlyRegion)
return resultRegion(target.position)
}
}
let me = Ship(position: Position(x: 0, y: 0), firingRange: 100, unsafeRange: 20)
let target = Ship(position: Position(x: 77, y: -10), firingRange: 100, unsafeRange: 20)
let friendly = Ship(position: Position(x: 59, y: 8), firingRange: 100, unsafeRange: 20)
me.canSafelyEngageShip(target: target, friendly: friendly)
// output: true
POP、泛型和函数式编程的结合
我们可以定义一个操作过程,传入一个参数产生一个结果。这种基础过程遍布我们的应用中:
enum Result<T> {
case success(T)
case failed(Error)
}
typealias fu_operation<E, T> = (E) -> Result<T>
这里定义了一个枚举的结果类型,用泛型表示结果中的实际类型。定义的operation表示输入一个E类型的数据,产生一个Result<T>
类型的结果。
然后我们可以定义一个combine函数,用于结合两个operation:
func combine<E, T, H>(op1: @escaping fu_operation<E, T>, op2: @escaping fu_operation<T, H>) -> fu_operation<E, H> {
return { params in
let rst = op1(params)
switch rst {
case .success(let value):
return op2(value)
case .failed(let e):
return .failed(e)
}
}
}
测试一下,a是将字典所有key值取出来编程数组的过程,b是把数组转成可读字符串的过程:
let a: fu_operation<[String: Any] ,[String]> = { params in
return .success(
params.reduce([String](), { (rst: [String], couple: (key: String, value: Any)) -> [String] in
var newRst: [String] = rst
newRst.append(couple.key)
return newRst
})
)
}
let b: fu_operation<[String], String> = {params in
var str = ""
for value in params {
str.append(value + " ,")
}
return .success(str)
}
combine(op1: a, op2: b)(["a": 1, "b": 2])
//输出为:
//success("b ,a ,")
为了方便使用,我们先定义一个类,以后也可以在其中做扩展:
class st_operation<E, T> {
var op: fu_operation<E, T> = { _ in .failed(NSError())}
init() {
}
init(with operation: @escaping (E) -> Result<T>) {
self.op = operation
}
}
以一个数据请求动作为例,拿到的数据通常是JSON转换成的Dictionary,所以我们定义一个协议:
protocol ConstructableFromDictionary {
static func generate(from dictionary: [String: Any]) -> Self?
}
接着我们定义我们的API动作,动作有两个操作,一个是发起请求获取源数据,一个是转换元数据到我们定义的模型:
typealias Parameters = [String: Any]
class API<T: ConstructableFromDictionary> {
var requestOperation: st_operation<Parameters, [String: Any]>
= st_operation { .success(Request.GET(params: $0)) }
var transform: st_operation<[String: Any], T> = st_operation {
if let rst = T.generate(from: $0) { return .success(rst) }
else { return .failed(NSError()) }
}
func request(params: Parameters?) throws -> T {
// 首先结合两个操作,然后返回其中的结果
let op = combine(op1: self.requestOperation.op, op2: self.transform.op)
let result = op(params ?? [:])
switch result {
case .success(let value):
return value
case .failed(let error):
throw error
}
}
}
假如我们现在需要拉取一个水果的信息,我们可以先定义一个水果的类,使他实现协议:
class Fruit: ConstructableFromDictionary {
var name: String = ""
required init() {
}
static func generate(from dictionary: [String : Any]) -> Self? {
let fruit = self.init()
fruit.name = dictionary["name"] as? String ?? ""
return fruit
}
}
这样一来,我们不用写其他任何多余代码,就可以利用基础的API类做到数据请求。
let fruitAPI = API<Fruit>()
do {
let fruit = try fruitAPI.request(params: nil)
fruit.name // Get fruit's name success, yeah
} catch {
// deal error
}
总结
- Swift带给我们的新体验是Amazing的,虽然POP或IOP概念很早就有,但是结合泛型和函数式编程,学习它而带给我们的是思维上的进化或者革命,让我们拥有更新的视角。
- 对于iOS而言,实际应用中,OOP和POP混合使用是必不可少的,POP的加入的意义是使代码更规范、灵活,使结构和逻辑更清晰易懂。
- 以上所有代码都是Swift-Only的,所以在以后跨平台上,Swift和POP会更有优势。当然在iOS上OC还是舍弃不掉的,OC的优势是更灵活的Runtime,实际应用中更需各取所长。写Swift代码也尽量避免rewrite OC code。
参考
- [B]Functional Swift - Chris Eidhof, Florian Kugler, Wouter Swierstra
- Practical Protocol Oriented Programming