Swift:面向协议的网络请求

前言

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 {
  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 的邂逅

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容