Home POP, 泛型和函数式思想在iOS中的应用
Post
Cancel

POP, 泛型和函数式思想在iOS中的应用

什么是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

image

父类初始化时要求子类的约束条件。

- (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。

参考

扩展阅读

This post is licensed under CC BY 4.0 by the author.