使用swift泛型构建具有高测试性的网络层

之前使用swift构建网络层使用的方法完全是Objective-C的实现方法,没有充分发挥swift的优势。

本文demo。

先说一说之前网络层的实现,首先就是有一个Network的工具类,作为对网络请求库(可以是URLSessionAlamofire)的一层封装,作为实际发起网络请求的类,一般情况下,我们会针对不同的模块进一步封装相应的工具类,例如:

class StackDataTool {
    class func getAllStock(completion: @escaping ([Stock]? -> ())) {
        //Network.request...
    }
    class func getLikedStock(completion: @escaping ([Stock]? -> ())) {
        //Network.request...
    }
}

我们在这里进行一层封装的目的:

  • 可以对请求的入参(比如userId等)进行配置
  • 可以对返回数据进行处理
  • 可以返回我们需要的数据模型

但是这种方法也有弊端,它发起网络请求,解析数据,实例化对象,但是网络可能出错,解析数据可能出错,甚至还有接口返回的业务错误,这都导致测试这个方法变得困难,接下来就来简化这个模式。

首先我们来创建一个Resource的结构体(使用class类型应该也是可以的),为了简单,我们只让它包含接口的URL解析函数的属性,解析函数用来解析从服务器返回的数据。

struct Resource {
    let url: URL
    let parse: (Data) -> T?
}

我们使用泛型来表示我们需要的数据类型,因为解析数据可能会失败,所以我们使用了可选类型。

在创建了一个Resource的对象以后,接下来我们来我们来重新整理一下StackDataTool具体请求的方法(这里使用URLSession作为例子)。

class StackDataTool {

    class func load(resource: Resource, completion: @escaping (A?) -> ()) {
        let request = URLRequest(url: resource.url)
        let session = URLSession.shared
        let task = session.dataTask(with: request) { (data, response, error) in

            if let response = response as? HTTPURLResponse, response.statusCode >= 200 && response.statusCode < 300, let data = data {
                completion(resource.parse(data))
            } else {
                completion(nil)
            }
        }
        task.resume()
    }
}

我们就会发现,这样写这个请求方法的通用性变得很高,只要创建对应的Resource,就可以使用这个方法进行任意的网络请求并按照我们给出的解析方法解析出我们需要的数据模型,那我们不妨把这个方法做成URLSession的扩展方法。

extension URLSession {

    func load(resource: Resource, completion: @escaping (A?) -> ()) {

        let request = URLRequest(url: resource.url)
        let session = URLSession.shared
        let task = session.dataTask(with: request) { (data, response, error) in

            if let response = response as? HTTPURLResponse, response.statusCode >= 200 && response.statusCode < 300, let data = data {
                completion(resource.parse(data))
            } else {
                completion(nil)
            }
        }
        task.resume()
    }
}

进一步优化,我们不妨把解析的过程放进URLSession中,创建一个Result的枚举值,来表示最后的结果,Resource的解析结果就可以直接解析成success或者fail,并把数据模型或者错误信息返回,最终我们得到的就是下面这种模式。

import PlaygroundSupport
import UIKit

PlaygroundPage.current.needsIndefiniteExecution = true


enum Result {
    case success(A)
    case fail(Error)
}

struct Resource {
    let url: URL
    let parse: ([String: Any]) -> Result
}

extension URLSession {

    func load(resource: Resource, completion: @escaping (Result) -> ()) {

        let request = URLRequest(url: resource.url)
        let session = URLSession.shared
        let task = session.dataTask(with: request) { (data, response, error) in

            if let response = response as? HTTPURLResponse, response.statusCode >= 200 && response.statusCode < 300, let data = data {
                do {
                    let object = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] ?? [:]
                    completion(resource.parse(object))
                } catch {
                    completion(.fail(error))
                }
            } else if let e = error {
                completion(.fail(e))
            }
        }
        task.resume()
    }
}

class Stock {
    var object: [String: Any]
    init(obj: [String: Any]) {
        self.object = obj
    }
}

let url = URL(string: "")!
let resource = Resource(url: url) { (object) -> Result in
    return .success(Stock(obj: object))
}
URLSession.shared.load(resource: resource) { (result) in
    switch result {
    case .success(let r):
        print(r.object)
    case .fail(let e):
        dump(e)
    }
}

因为针对某些固定的请求,它们的参数是固定的,因此可以对相对的对象进行扩展,方便快速获取Resource,例如:

extension Stock {
    class var allStock: Resource {
        return Resource(url: url) { (object) -> Result in
            return .success(Stock(obj: object))
        }
    }
}

我们还可以对相应的错误信息进行细致的划分,例如一些服务器返回的统一错误、解析错误、业务特定错误都进行罗列:

enum ServerError: Error {
    case unknowError
    case sessionExpired
    case noAccess
}
// 解析错误
struct ParseError: Error {}

func checkForError(dic: [String: Any], success:((_ dataContent: [String: Any]) -> (Result))) -> Result {
    let code = dic.read(key: "code", defaultValue: -1)
    let content = dic.read(key: "data", defaultValue: [String: Any]())
    switch code {
    case 0:
        return success(content)
    case -2:
        return .fail(ServerError.sessionExpired)
    case -3:
        return .fail(ServerError.noAccess)
    default:
        return .fail(ServerError.unknowError)
    }
}

let resource = Resource(method: "GET", url: url, para: ["width" : "640", "height": "1136"], header: header) { (object) -> Result in

    return checkForError(dic: object) { (content) -> (Result) in
        return .success(Stock(dic: content))
    }
}

URLSession.shared.load(resource: resource) { (result) in
    switch result {
    case .success(let r):
        print(r.object)
    case .fail(let e):
        switch e {
        case let serverError as ServerError:
            switch serverError {
            case .noAccess:
                print("用户没有权限")
            case .sessionExpired:
                print("会话失效")
            default:
                print("未知错误")
            }
        case _ as ParseError:
            print("解析错误")
        default:
            print(e.localizedDescription)
        }
    }
}

之后还可以使用JSONDecoder让这个网络层更加通用、简单,详情见下一篇文章。

你可能感兴趣的:(使用swift泛型构建具有高测试性的网络层)