封装一个 Swift-Style 的网络模块

Swift 跟 OC 有着完全不同的设计哲学,它鼓励你使用 protocol 而不是 super class,使用 enum 和 struct 而不是 class,它支持函数式特性、范型和类型推导,让你可以轻松封装异步过程,用链式调用避免 callback hell。如果你还是用 OC 的思维写着 Swift 代码,那可以说是一种极大的资源浪费,你可能还会因为 Swift 弱鸡的反射而对它感到不满,毕竟 Swift 在强类型和安全性方面下足了功夫,如果不使用 OC 的 runtime,在动态性方面是远不如 OC 的。

OOP 和消息传递非常适合 UI 编程,在这方面来说 OC 是非常称职的,整个 Cocoa Touch 框架也都是面向对象的,所以对于 iOS 开发来说,不管你使用什么语言,都必须熟悉 OOP。在 UI 构建方面,无论是 Swift 还是 OC,无非都是调用 API 罢了,在有自动提示的情况下,其实编码体验都差不多。那 Swift 相比于 OC 的优势到底体现在什么地方呢,我认为是 UI 以外的地方,跟 UI 关系越小,Swift 能一展拳脚的余地就越大,譬如网络层。

讲到网络层就绕不开 Alamofire,Alamofire 几乎是现在用 Swift 开发 iOS App 的标配,它是个很棒的库,几乎能满足所有网络方面的日常需求,但如果对它再封装一下的话,不仅使用起来更得心应手,而且能将第三方库与业务代码解耦,以后万一要更换方案会更加方便。

Alamofire 使用 Result 来表示请求返回的结果,它是个 enum,长这样:

public enum Result<Value, Error : ErrorType> {
    case Success(Value)
    case Failure(Error)
    /// Returns `true` if the result is a success, `false` otherwise.
    public var isSuccess: Bool { get }
    /// Returns `true` if the result is a failure, `false` otherwise.
    public var isFailure: Bool { get }
    /// Returns the associated value if the result is a success, `nil` otherwise.
    public var value: Value? { get }
    /// Returns the associated error value if the result is a failure, `nil` otherwise.
    public var error: Error? { get }
}

我们可以对它进行扩展,让它支持链式调用:

import Foundation
import Alamofire

extension Result {

    // Note: rethrows 用于参数是一个会抛出异常的闭包的情况,该闭包的异常不会被捕获,会被再次抛出,所以可以直接使用 try,而不用 do-try-catch

    // U 可能为 Optional
    func map<U>(@noescape transform: Value throws -> U) rethrows -> Result<U, Error> {
        switch self {
        case .Failure(let error):
            return .Failure(error)
        case .Success(let value):
            return .Success(try transform(value))
        }
    }

    // 若 transform 的返回值为 nil 则作为异常处理
    func flatMap<U>(@noescape transform: Value throws -> U?) rethrows -> Result<U, Error> {
        switch self {
        case .Failure(let error):
            return .Failure(error)
        case .Success(let value):
            guard let transformedValue = try transform(value) else {
                return .Failure(SYError.errorWithCode(.TransformFailed) as! Error)
            }
            return .Success(transformedValue)
        }
    }

    // 适用于 transform(value) 之后可能产生 error 的情况
    func flatMap<U>(@noescape transform: Value throws -> Result<U, Error>) rethrows -> Result<U, Error> {
        switch self {
        case .Failure(let error):
            return .Failure(error)
        case .Success(let value):
            return try transform(value)
        }
    }

    // 处理错误,并向下传递
    func mapError(@noescape transform: Error throws -> NSError) rethrows -> Result<Value, NSError> {
        switch self {
        case .Failure(let error):
            return .Failure(try transform(error))
        case .Success(let value):
            return .Success(value)
        }
    }

    // 处理数据(不再向下传递数据,作为数据流的终点)
    func handleValue(@noescape handler: Value -> Void) {
        switch self {
        case .Failure(_):
            break
        case .Success(let value):
            handler(value)
        }
    }

    // 处理错误(终点)
    func handleError(@noescape handler: Error -> Void) {
        switch self {
        case .Failure(let error):
            handler(error)
        case .Success(_):
            break
        }
    }
}

有了这个扩展我们就可以定义一个parseResult的方法,对返回结果进行处理,像这样:

func parseResult(result: Result<AnyObject, NSError>, responseKey: String) -> Result<AnyObject, NSError> {
    return result
        .flatMap { $0 as? [String: AnyObject] }
        .flatMap(self.checkJSONDict) // 解析错误信息并进行打印,然后继续向下传递,之后业务方可自由选择是否进一步处理错误
        .flatMap { $0.valueForKey(responseKey) }
}

checkJSONDict用来处理服务器返回的错误信息,具体的处理逻辑不同项目都不一样,主要看跟服务器的约定,我就不细说了。valueForKey是对Dictionary的扩展,可以通过字符串拿到返回的 JSON 数据中需要的部分(先转换成[String: AnyObject]),支持用"."分隔 key,从而取得嵌套对象。譬如这样一个东西:

{
  key1: value1,
  key2: { nest: value2 }
  key3: { nest1: { nest2: value3 } }
}

你可以用"key2.nest"拿到value2,用"key3.nest1.nest2"拿到value3。我用reduce实现了这个功能:

extension Dictionary {
    var dictObject: AnyObject? { return self as? AnyObject }

    func valueForKey(key: Key) -> Value? {
        guard let stringKey = key as? String 
            where stringKey.containsString(".") else { return self[key] }

        let keys = stringKey.componentsSeparatedByString(".")
        guard !keys.isEmpty else { return nil }

        let results: AnyObject? = keys.reduce(dictObject, combine: fetchValueInObject)
        return results as? Value
    }
}

func fetchValueInObject(object: AnyObject?, forKey key: String) -> AnyObject? {
    return (object as? [String: AnyObject])?[key]
}

有了parseResult之后,我们就可以轻松封装请求过程了:

/**
 Fetch raw object

 - parameter api:              API address
 - parameter method:           HTTP method, default = POST
 - parameter parameters:       Request parameters, default = nil
 - parameter responseKey:      Key of target value, use '.' to get nested objects, e.g. "data.vehicle_list"
 - parameter jsonArrayHandler: Handle result with raw object

 - returns: Optional request object which is cancellable.
 */
func fetchDataWithAPI(api: API,
                   method: Alamofire.Method = .POST,
               parameters: [String: AnyObject]? = nil,
              responseKey: String,
 networkCompletionHandler: NetworkCompletionHandler) -> Cancellable? {

    guard let url = api.url else {
        printLog("URL Invalid: \(api.rawValue)")
        return nil
    }

    let params = configParameters(parameters)

    return Alamofire.request(method, url, parameters: params).responseJSON {
        networkCompletionHandler(self.parseResult($0.result, responseKey: responseKey))
    }
}

API是一个枚举,有一个url的计算属性,用来返回 API 地址,configParameters用来配置请求参数,也跟具体项目有关,就不展开了,method可以设置一个项目中常用的 HTTP Method 作为默认参数。这个方法会返回一个Cancellable,长这样:

protocol Cancellable {
    func cancel()
}

extension Request: Cancellable {}

Request本来就实现了cancel方法,所以只要显式地声明一下它遵守Cancellable协议就行了,使用的时候像这样:

let task = NetworkManager.defaultManager
    .fetchDataWithAPI(.ModelList, responseKey: "data.model_list") {
        // ...
}

在请求完成之前,随时可以调用task?.cancel() 来取消这个网络任务。

当然如果你想在网络模块中把 JSON 直接转化成 Model 也是可以的,我个人倾向于使用 ObjectMapper 来构建网络 Model 层,于是就可以对外提供两个直接取得 Model 和 Model 数组的方法:

/**
 Fetch JSON model

 - parameter api:              API address
 - parameter method:           HTTP method, default = POST
 - parameter parameters:       Request parameters, default = nil
 - parameter responseKey:      Key of target value, use '.' to get nested objects, e.g. "data.vehicle_list"
 - parameter jsonArrayHandler: Handle result with model

 - returns: Optional request object which is cancellable.
 */
func fetchJSONWithAPI<T: Mappable>(api: API,
                                method: Alamofire.Method = .POST,
                            parameters: [String: AnyObject]? = nil,
                           responseKey: String,
                           jsonHandler: Result<T, NSError> -> Void) -> Cancellable? {

    return fetchDataWithAPI(api, method: method, parameters: parameters, responseKey: responseKey) {
        jsonHandler($0.flatMap(=>))
    }
}

/**
 Fetch JSON array

 - parameter api:              API address
 - parameter method:           HTTP method, default = POST
 - parameter parameters:       Request parameters, default = nil
 - parameter responseKey:      Key of target value, use '.' to get nested objects, e.g. "data.vehicle_list"
 - parameter jsonArrayHandler: Handle result with model array

 - returns: Optional request object which is cancellable.
 */
func fetchJSONArrayWithAPI<T: Mappable>(api: API,
                                     method: Alamofire.Method = .POST,
                                 parameters: [String: AnyObject]? = nil,
                                responseKey: String,
                           jsonArrayHandler: Result<[T], NSError> -> Void) -> Cancellable? {

    return fetchDataWithAPI(api, method: method, parameters: parameters, responseKey: responseKey) {
        jsonArrayHandler($0.flatMap(=>))
    }
}

=>是我自定义的操作符,它有两个重载版本,都满足flatMap的参数要求:

postfix operator => {}

postfix func =><T: Mappable>(object: AnyObject) -> T? {
    return Mapper().map(object)
}

postfix func =><T: Mappable>(object: AnyObject) -> [T]? {
    return Mapper().mapArray(object)
}

于是就可以在业务代码中直接这样:

class TableViewController: UITableViewController {
    // ...
    var results: [Demo]? {
        didSet {
            tableView.reloadData()
        }
    }

    func fetchData() {
        let task = NetworkManager.defaultManager
            .fetchJSONArrayWithAPI(.Demo, responseKey: "data.demo_list") { 
                self.results = $0.value
        }
    }
}

到此一个简洁方便的网络模块就差不多成型了,别忘了为你的模块添加单元测试,这会让模块的使用者对你的代码更有信心,而且在测试过程中会让你发现一些开发过程中的思维盲区,还能帮你优化设计,毕竟良好的可测试性在某种程度上就意味着良好的可读性和可维护性。

有什么建议欢迎在评论中指出 ^ ^

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏源码之家

word如何自动分割成多个文档

3825
来自专栏JAVA高级架构开发

Java 11 正式发布,这 8 个逆天新特性教你写出更牛逼的代码

美国时间 09 月 25 日,Oralce 正式发布了 Java 11,这是据 Java 8 以后支持的首个长期版本。

2040
来自专栏岑玉海

Spark源码系列(九)Spark SQL初体验之解析过程详解

好久没更新博客了,之前学了一些R语言和机器学习的内容,做了一些笔记,之后也会放到博客上面来给大家共享。一个月前就打算更新Spark Sql的内容了,因为一些别的...

4045
来自专栏非典型技术宅

Swift实践:使用CoreData存储多种数据类的通讯录1. CoreData支持存储数据类型2. 使用CoreData存储多种数据类的通讯录3. Codable

1793
来自专栏nimomeng的自我进阶

Collection官方文档

a) Keys必须实现NSCopying协议。添加成员的方法并不将每一个key直接进行添加,而是将每一个key进行copy并将copy后对象添加...

1404
来自专栏令仔很忙

观察者模式和Spring的结合

这周给分了一个任务,就是对查询回来的数据进行各种各样的过滤,有七种不同的过滤条件。过滤条件是在数据库中存着的。在我们项目中有一个热发,就是定时的从数据库中把数...

952
来自专栏黄Java的地盘

[翻译]WebSocket协议第二章——Conformance Requirements

本文为WebSocket协议的第二章,本文翻译的主要内容为WebSocket协议中相关术语的介绍。

841
来自专栏一枝花算不算浪漫

Matcher类的简单使用

2897
来自专栏用户2442861的专栏

如何给10^7个数据量的磁盘文件排序

第一节、如何给磁盘文件排序 问题描述: 输入:一个最多含有n个不重复的正整数(也就是说可能含有少于n个不重复正整数)的文件,其中每个数都小于等于n,且n=...

632
来自专栏函数式编程语言及工具

Akka(4): Routers - 智能任务分配

    Actor模式最大的优点就是每个Actor都是一个独立的任务运算器。这种模式让我们很方便地把一项大型的任务分割成若干细小任务然后分配给不同的Actor去...

2715

扫码关注云+社区