Swift:面向协议的网络请求

5,126 阅读6分钟

前言

class Light {
  func 插电() {}
  func 打开() {}
  func 增大亮度() {}
  func 减小亮度() {}
}

class LEDLight: Light {}
class DeskLamp: Light {}

func 打开(物体: Light) {
  物体.插电()
  物体.打开()
}

func main() {
  打开(物体: DeskLamp())
  打开(物体: LEDLight())
}

在上述面向对象的实现中打开方法似乎只局限于Light这个类和他的派生类。如果我们想描述打开这个操作并且不单单局限于Light这个类和他的派生类,(毕竟柜子、桌子等其他物体也是可以打开的)抽象打开这个操作,那么protocol就可以派上用场了。

protocol Openable {
  func 准备工作()
  func 打开()
}

extension Openable {
  func 准备工作() {}
  func 打开() {}
}

class LEDLight: Openable {}
class DeskLamp: Openable {}
class Desk: Openable {}

func 打开<T: Openable>(物体: T) {
  物体.准备工作()
  物体.打开()
}

func main() {
  打开(物体: Desk())
  打开(物体: LEDLight())
}

普通的网络请求

  // 1.准备请求体
  let urlString = "https://www.baidu.com/user"
  guard let url = URL(string: urlString) else {
    return
  }
  let body = prepareBody()
  let headers = ["token": "thisisatesttokenvalue"]
  var request = URLRequest(url: url)
  request.httpBody = body
  request.allHTTPHeaderFields = headers
  request.httpMethod = "GET"

  // 2.使用URLSeesion创建网络任务
  URLSession.shared.dataTask(with: request) { (data, response, error) in
    if let data = data {
      // 3.将数据反序列化
    }
  }.resume()

我们可以看到发起一个网络请求一般会有三个步骤

  • 准备请求体(URL、parameters、body、headers...)
  • 使用框架创建网络任务(URLSession、Alamofire、AFN...)
  • 将数据反序列化(Codable、Protobuf、SwiftyJSON、YYModel...)

我们可以把这三个步骤进行抽象,用三个protocol进行规范. 规范好之后,再由各个类型实现这三个协议,就可以随意组合使用.

抽象网络请求步骤

Parsable

首先我们定义Parsable协议来抽象反序列化这个过程

protocol Parsable {
  // ps: Result类型下边会声明,这里姑且可以认为函数返回了`Self`
  static func parse(data: Data) -> Result<Self>
}

Parsable协议定义了一个静态方法,这个方法可以从Data -> Self 例如User遵循Parsable协议,就要实现从Data转换到User的parse(:)方法

struct User {
  var name: String
}
extension User: Parsable {
  static func parse(data: Data) -> Result<User> {
    // ...实现Data转User
  }
}

Codable

我们可以利用swift协议扩展的特性给遵循Codable的类型添加一个默认的实现

extension Parsable where Self: Decodable {
  static func parse(data: Data) -> Result<Self> {
    do {
      let model = try decoder.decode(self, from: data)
      return .success(model)
    } catch let error {
      return .failure(error)
    }
  }
}

这样User如果遵循了Codable,就无需实现parse(:)方法了 于是反序列化的过程就变这样简单的一句话

extension User: Codable, Parsable {}

URLSession.shared.dataTask(with: request) { (data, response, error) in
    if let data = data {
      // 3.将数据反序列化
        let user = User.parse(data: data)
    }

到这里可以想一个问题,如果data是个模型数组该怎么办?是不是在Parsable协议里再添加一个方法返回一个模型数组?然后再实现一遍?

public protocol Parsable {
  static func parse(data: Data) -> Result<Self>
// 返回一个数组
  static func parse(data: Data) -> Result<[Self]>
}

这样也不是不行,但是还有更swift的方法,这种方法swift称之为条件遵循

// 当Array里的元素遵循Parsable以及Decodable时,Array也遵循Parsable协议
extension Array: Parsable where Array.Element: (Parsable & Decodable) {}
URLSession.shared.dataTask(with: request) { (data, response, error) in
    if let data = data {
      // 3.将数据反序列化
        let users = [User].parse(data: data)
    }

从这里可以看到swift协议是非常强大的,使用好了可以减少很多重复代码,在swift标准库中有很多这样的例子。

protobuf

当然,如果你使用SwiftProtobuf,也可以提供它的默认实现

extension Parsable where Self: SwiftProtobuf.Message {
  static func parse(data: Data) -> Result<Self> {
    do {
      let model = try self.init(serializedData: data)
      return .success(model)
    } catch let error {
      return .failure(error)
    }
  }
}

反序列化的过程也和刚才的例子一样,调用parse(:)方法即可

Request

现在我们定义Request协议来抽象准备请求体这个过程

protocol Request {
  var url: String { get }
  var method: HTTPMethod { get }
  var parameters: [String: Any]? { get }
  var headers: HTTPHeaders? { get }
  var httpBody: Data? { get }

  /// 请求返回类型(需遵循Parsable协议)
  associatedtype Response: Parsable
}

我们定义了一个关联类型:遵循ParsableResponse 是为了让实现这个协议的类型指定这个请求返回的类型,限定Response必须遵循Parsable是因为,我们会用到parse(:)方法来进行反序列化。

我们来实现一个通用的请求体

struct NormalRequest<T: Parsable>: Request {
  var url: String
  var method: HTTPMethod
  var parameters: [String: Any]?
  var headers: HTTPHeaders?
  var httpBody: Data?

  typealias Response = T

  init(_ responseType: Response.Type,
       urlString: String,
       method: HTTPMethod = .get,
       parameters: [String: Any]? = nil,
       headers: HTTPHeaders? = nil,
       httpBody: Data? = nil) {
    self.url = urlString
    self.method = method
    self.parameters = parameters
    self.headers = headers
    self.httpBody = httpBody
  }
}

是这样使用的

let request = NormalRequest(User.self, urlString: "https://www.baidu.com/user")

如果服务端有一组接口 https://www.baidu.com/user https://www.baidu.com/manager https://www.baidu.com/driver 我们可以定义一个BaiduRequest,把URL或者公共的headers和body拿到BaiduRequest管理

// BaiduRequest.swift
private let host = "https://www.baidu.com"

enum BaiduPath: String {
  case user = "/user"
  case manager = "/manager"
  case driver = "/driver"
}

struct BaiduRequest<T: Parsable>: Request {
  var url: String
  var method: HTTPMethod
  var parameters: [String: Any]?
  var headers: HTTPHeaders?
  var httpBody: Data?

  typealias Response = T

  init(_ responseType: Response.Type,
       path: BaiduPath,
       method: HTTPMethod = .get,
       parameters: [String: Any]? = nil,
       headers: HTTPHeaders? = nil,
       httpBody: Data? = nil) {
    self.url = host + path.rawValue
    self.method = method
    self.parameters = parameters
    self.httpBody = httpBody
    self.headers = headers
  }
}

创建也很简单

let userRequest = BaiduRequest(User.self, path: .user)
let managerRequest = BaiduRequest(Manager.self, path: .manager, method: .post)

Client

最后我们定义Client协议,抽象发起网络请求的过程

enum Result<T> {
  case success(T)
  case failure(Error)
}
typealias Handler<T> = (Result<T>) -> ()

protocol Client {
// 接受一个遵循Parsable的T,最后回调闭包的参数是T里边的Response 也就是Request协议定义的Response
  func send<T: Request>(request: T, completionHandler: @escaping Handler<T.Response>)
}

URLSession

我们来实现一个使用URLSessionClient

struct URLSessionClient: Client {
  static let shared = URLSessionClient()
  private init() {}

  func send<T: Request>(request: T, completionHandler: @escaping (Result<T.Response>) -> ()) {
    var urlString = request.url
    if let param = request.parameters {
      var i = 0
      param.forEach {
        urlString += i == 0 ? "?\($0.key)=\($0.value)" : "&\($0.key)=\($0.value)"
        i += 1
      }
    }
    guard let url = URL(string: urlString) else {
      return
    }
    var req = URLRequest(url: url)
    req.httpMethod = request.method.rawValue
    req.httpBody = request.httpBody
    req.allHTTPHeaderFields = request.headers

    URLSession.shared.dataTask(with: req) { (data, respose, error) in
      if let data = data {
        // 使用parse方法反序列化
        let result = T.Response.parse(data: data)
        switch result {
        case .success(let model):
          completionHandler(.success(model))
        case .failure(let error):
          completionHandler(.failure(error))
        }
      } else {
        completionHandler(.failure(error!))
      }
    }
  }
}

三个协议实现好之后 例子开头的网络请求就可以这样写了

let request = NormalRequest(User.self, urlString: "https://www.baidu.com/user")
URLSessionClient.shared.send(request) { (result) in
  switch result {
     case .success(let user):
       // 此时拿到的已经是User实例了
       print("user: \(user)")
     case .failure(let error):
       printLog("get user failure: \(error)")
     }
}

Alamofire

当然也可以用Alamofire实现Client

struct NetworkClient: Client {
  static let shared = NetworkClient()

  func send<T: Request>(request: T, completionHandler: @escaping Handler<T.Response>) {
    let method = Alamofire.HTTPMethod(rawValue: request.method.rawValue) ?? .get
    var dataRequest: Alamofire.DataRequest

    if let body = request.httpBody {
      var urlString = request.url
      if let param = request.parameters {
        var i = 0
        param.forEach {
          urlString += i == 0 ? "?\($0.key)=\($0.value)" : "&\($0.key)=\($0.value)"
          i += 1
        }
      }
      guard let url = URL(string: urlString) else {
        print("URL格式错误")
        return
      }
      var urlRequest = URLRequest(url: url)
      urlRequest.httpMethod = method.rawValue
      urlRequest.httpBody = body
      urlRequest.allHTTPHeaderFields = request.headers
      dataRequest = Alamofire.request(urlRequest)
    } else {
      dataRequest = Alamofire.request(request.url,
                                      method: method,
                                      parameters: request.parameters,
                                      headers: request.headers)
    }

    dataRequest.responseData { (response) in
      switch response.result {
      case .success(let data):
        // 使用parse(:)方法反序列化
        let parseResult = T.Response.parse(data: data)
        switch parseResult {
        case .success(let model):
          completionHandler(.success(model))
        case .failure(let error):
          completionHandler(.failure(error))
        }
      case .failure(let error):
        completionHandler(.failure(error))
      }
    }
  }

  private init() {}
}

我们试着发起一组网络请求

let userRequest = BaiduRequest(User.self, path: .user)
let managerRequest = BaiduRequest(Manager.self, path: .manager, method: .post)

NetworkClient.shared.send(managerRequest) { result in
    switch result {
     case .success(let manager):
       // 此时拿到的已经是Manager实例了
       print("manager: \(manager)")
     case .failure(let error):
       printLog("get manager failure: \(error)")
     }
}

总结

我们用三个protocol抽象了网络请求的过程,让网络请求变得很灵活,你可以随意组合各种实现,不同的请求体配不同的序列化方式或者不同的网络框架。可以使用URLSession + Codable,也可以使用Alamofire + Protobuf等等,极大的方便了我们日常开发。

引用

喵神的这篇文章是我学习面向协议的开始,给了我极大的启发:面向协议编程与 Cocoa 的邂逅