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()
複製代碼

咱們能夠看到發起一個網絡請求通常會有三個步驟swift

  • 準備請求體(URL、parameters、body、headers...)
  • 使用框架建立網絡任務(URLSession、Alamofire、AFN...)
  • 將數據反序列化(Codable、Protobuf、SwiftyJSON、YYModel...)

咱們能夠把這三個步驟進行抽象,用三個protocol進行規範. 規範好以後,再由各個類型實現這三個協議,就能夠隨意組合使用.api

抽象網絡請求步驟

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稱之爲條件遵循post

// 當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 的邂逅

相關文章
相關標籤/搜索