面向協議編程(POP)實戰-網絡層封裝

上一篇Swift面向協議編程(POP)中,咱們瞭解了POP,以及POP解決的問題,優勢和特性。本篇咱們咱們POP來對網絡層封裝,體驗POP帶來的解耦合易於測試,強大的擴展性編程

1、準備工做

首先咱們須要知道,網絡層通常狀況下作的是從一個API請求到JSON數據,而後轉化爲一個可用的實例對象。 那麼咱們用一個登陸的例子來說解一下這個過程。 登陸接口: http://www.jihuabiao.net:8890/plan/freeuser/login 參數: account,password 返回結構:json

{
    loginUser = {
        id = 2c93167b6cb7dd34016cc1a32802002a;
        nickname = jensen;
        phone = 199****1676;
    };
}
複製代碼

2、開始實踐

首先新建LoginUser.swift模型:swift

struct LoginUser {
    let id: String
    let nickname: String
    let phone: String
    
    init?(data: NSDictionary) {
    
        guard let loginUser = data["loginUser"] as? NSDictionary else {
            return nil
        }
        guard let id = loginUser["id"] as? String else {
            return nil
        }
        guard let nickname = loginUser["nickname"] as? String else {
            return nil
        }
        guard let phone = loginUser["phone"] as? String else {
            return nil
        }
        self.id = id
        self.nickname = nickname
        self.phone = phone
    }
}
複製代碼

init中傳入一個NSDictionary,建立一個LoginUser實例。 如何使用POP的方式從URL請求到數據並生成對應的LoginUser,是這裏的重點。 咱們知道Request是網絡請求的入口,因此能夠直接建立一個網絡請求協議,網絡請求須要知道路徑,方法,參數等等。api

enum JNHTTPMethod {
    case GET
    case POST
}

protocol JNRequest {
   
    var host : String {get}
    var path : String {get}
    var method : JNHTTPMethod {get}
    var parameter: [String: Any] { get }
    
}
複製代碼
  • 請求地址由hostpath拼接而成
  • method支持GETPOST,本例使用POST
  • parameter是請求的參數

建立LoginRequest實現JNRequestbash

struct LoginRequest: JNRequest {
    var host: String  {
        return "http://www.jihuabiao.net:8890/plan/"
    }
    var path: String {
        return "freeuser/login"
    }
    let method: JNHTTPMethod = .POST
    var parameter: [String : Any]
}
複製代碼
  • 設置host路徑和path路徑
  • 指定methodPOST

這個時候,咱們已經有了發請求的條件(路由,方法,參數)。下一步就須要發送請求了。咱們能夠爲JNRequest擴展發送請求的能力,這樣可讓每個請求都是用同樣的方法發送的。網絡

extension JNRequest {
    func sendRequest(hander:@escaping(LoginUser)->Void) {
        //...
    }
}
複製代碼
  • JNRequest擴展sendRequest
  • 逃逸閉包hander可將請求結果返回到外界

這裏返回的是LoginUser模型,這樣的話sendRequest方法就只能支持這個登陸請求。咱們可使用關聯類型解決這個問題,使請求通常化.閉包

protocol JNRequest {
   //.....
    associatedtype Response
}

struct LoginRequest: JNRequest {
    typealias Response = LoginUser
    //....
}
extension JNRequest {
    func sendRequest(hander:@escaping(Response)->Void) {
    //...
    }
}
複製代碼
  • JNRequest協議中添加associatedtype Response
  • LoginRequest添加typealias Response = LoginUser,執行返回類型爲LoginUser
  • JNRequest的擴展方法sendRequest的逃逸閉包將返回類型改成Response

sendRequest發送方法中,咱們開始實現發起網絡請求的代碼。最近剛學完Alamofire,就直接使用Alamofire吧。post

func sendRequest(hander:@escaping(Response)->Void) {
        let url = self.host + self.path
        Alamofire.request(url, method: HTTPMethod(rawValue: httpMethod.rawValue)!, parameters: self.parameter).responseJSON { (response) in
            print(response)
        }
    }
複製代碼
  • 拼接url
  • 調用Alamofire.request請求數據
  • 使用JSON序列化器
  • 獲取到網絡返回的JSON數據

如今還差最後一步,將返回的JSON數據轉化爲LoginUser模型數據 咱們爲JNRequest協議添加方法測試

protocol JNRequest {
    //...
    func parse(data: NSDictionary) -> Response?
}
複製代碼

LoginRequest擴展實現:優化

extension LoginRequest {
    
    func parse(data: NSDictionary) -> LoginUser? {
        return LoginUser(data:data)
    }
}
複製代碼

sendRequest中調用序列化解析:

func sendRequest(hander:@escaping(Response?)->Void) {
        let url = self.host + self.path
        Alamofire.request(url, method: HTTPMethod(rawValue: httpMethod.rawValue)!, parameters: self.parameter).responseJSON { (response) in
            switch response.result {
            case .success(let data):
                   let dic = data as? NSDictionary
                   if let res = self.parse(data: dic!) {
                    hander(res)
                 }else {
                    hander(nil)
                 }
                case .failure:
                    hander(nil)
            }
        }
    }
複製代碼

外界使用請求:

let request = LoginRequest(parameter: ["account":"19916721676","password":"123456"])
  request.sendRequest { (LoginUser) in
      print(LoginUser)
   }
複製代碼
  • 只須要建立request
  • 調用sendRequest發起請求

使用起來很是便捷,也能實現需求。可是這樣的實現很是糟糕。咱們看看JNRequest的定義和擴展:

protocol JNRequest {
   
    var host : String {get}
    var path : String {get}
    var httpMethod : JNHTTPMethod {get}
    var parameter: [String: Any] { get }
    
    associatedtype Response
    func parse(data: NSDictionary) -> Response?
}

extension JNRequest {
    func sendRequest(hander:@escaping(Response?)->Void) {
       //...
    }
}
複製代碼

上面的實現主要問題在於Request管理的東西太多.咱們對於Request的瞭解,它該作的事情應該是定義請求入口,保存請求的信息和響應類型。而這裏的Request保存host,還進行數據的解析成,這樣作就沒法在不修改請求的狀況下更改解析的方式,增長耦合度,不利於測試。發送請求也是它的一部分,這樣請求的具體實現就和請求產生耦合,這也是不合理的...

3、重構優化

鑑於上述實現存在一些問題,咱們着手重構代碼,解決上述存在的問題。 首先咱們先將sendRequest中剝離出來,咱們須要一個單獨的類型來負責發送請求。基於POP的設計方式,咱們定義以下協議:

protocol JNDataClient {
    var host: String { get }
   func send<T :JNRequest>(_ r : T, handler: @escaping(T.Response?)->Void)
}

複製代碼

JNRequest中含有關聯類型,因此咱們使用泛型,不能使用獨立的類型。對於一個模塊的請,host不該該在Request中設置,咱們將其移動到JNDataClient. 清除請求中的host以及send。並定義JNAlamofireClient實現JNDataClient協議:

struct JNAlamofireClient {
   
   var host: String  {
       return "http://www.jihuabiao.net:8890/plan/"
   }
   func send<T :JNRequest>(_ r : T, handler: @escaping(T.Response?)->Void) {

           let url = self.host + r.path
           Alamofire.request(url, method: HTTPMethod(rawValue: r.httpMethod.rawValue)!, parameters: r.parameter).responseJSON { (response) in
               switch response.result {
               case .success(let data):
                   let dic = data as? NSDictionary
                   if let res = r.parse(data: dic!) {
                       handler(res)
                   }else {
                       handler(nil)
                   }
               case .failure:
                   handler(nil)
               }
           }
   }
}
複製代碼

目前已經將發送請求和請求自己分離開,咱們定義了JNDataClient協議,這裏實現了JNAlamofireClient,使用Alamofire發送請求。未來咱們若是想要更換原生URLSession來發送請求,咱們能夠直接定義JNURLSessionClient實現JNDataClient,或者直接在本地獲取數據能夠定義JNLocalClient等。網絡層的具體實現和請求自己再也不耦合,更利於測試。 上述提到的問題,對象的解析不該該由Request來完成,應該交給Response。咱們新增一個協議,知足這個協議的須要實現parse方法:

protocol Decodable {
   static func parse(data : NSDictionary)->Self?
}
複製代碼

爲了保證全部的Response都能解析數據,咱們須要對Response實現Decodable協議,並移除Request的解析方法。

protocol JNRequest {
   
    var path : String {get}
    var httpMethod : JNHTTPMethod {get}
    var parameter: [String: Any] { get }
    
    associatedtype Response : Decodable
}
複製代碼

LoginUser擴展解析方法:

extension LoginUser : Decodable{
   static func parse(data: NSDictionary) -> LoginUser? {
       return LoginUser(data: data)!
   }
}
複製代碼

send中直接將解析交給T.Response:

if let dic = data as? NSDictionary {
                        if let res = T.Response.parse(data: dic) {
                            handler(res)
                        }else {
                            handler(nil)
                        }
                    }else {
                        handler(nil)
                    }
複製代碼

外界使用:

let request = LoginRequest(parameter: ["account":"19916721676","password":"123456"])
        JNAlamofireClient().send(request) {(LoginUser) in
            print(LoginUser!)
        }
複製代碼

咱們還能夠添加一個單例來減小請求時的建立開銷:

struct JNAlamofireClient {
    static let `default` = JNAlamofireClient()
}
複製代碼

若是須要建立其餘的請求, 能夠用和 LoginRequest 類似的方式,爲網絡層添加其餘的API 請求,只須要定義請求所必要的內容,而不用擔憂會觸及網絡方面的具體實現。 以上就是使用POP對網絡層封裝的實戰。

4、POP封裝帶來的好處

易於測試

如上述提到,咱們只是定義了JNDataClient協議,這樣咱們就能夠再也不侷限於特定的一種技術(URLSession,Alamofire,AFNetworking等)來實現請求的發送。咱們甚至能夠提供一組虛擬請求的響應,用來進行測試。

準備一個文本response.json:

{"loginUser":{"id":"2c93167b6cb7dd34016cc1a32802002a","nickname":"jensen","phone":"199****1676"}}
複製代碼

咱們能夠建立一個類型JNLocalClient,實現JNDataClient協議:

struct JNLocalClient {
    
    var host: String  {
        return ""
    }
    func send<T :JNRequest>(_ r : T, handler: @escaping(T.Response?)->Void) {
        
        switch r.path {
        case "freeuser/login":
            let fileURL = Bundle.main.path(forResource: "response", ofType: "json")
            if let data = try? String.init(contentsOfFile: fileURL!, encoding: .utf8) {
                let jsonData:Data = data.data(using: .utf8)!
                if let dic = try? JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) {
                    if let res = T.Response.parse(data: dic as! NSDictionary) {
                         handler(res)
                    }else {
                         handler(nil)
                    }
                }else {
                    handler(nil)
                }
            }else {
                handler(nil)
            }
        default:
            handler(nil)
        }
    }
}
複製代碼
  • 檢查輸入請求的path屬性,根據path不一樣,從bundle中讀取預先設定的文件數據。
  • 對返回的結果作JSON解析,而後調用Response的parse解析
  • 調用handler返回數據到外界。
  • 若是咱們須要增長其餘請求的測試,能夠添加新的case

這裏補充一點,咱們以前在設定Decodable協議時,設定parse傳入的參數必須是NSDictionary類型:

protocol Decodable {
   static func parse(data : NSDictionary)->Self?
}
複製代碼

因此在JNLocalClient中咱們須要本身解析成JSON,使用起來不太好用。使用POP的方式咱們能夠不限定單獨的類型,而是限定一個協議DecodeType

protocol DecodeType {
    func asDictionary() -> NSDictionary?;
}
複製代碼

爲可能傳入解析的類型好比NSDictionary,Data等添加擴展:

extension NSDictionary : DecodeType {
    func asDictionary() -> NSDictionary? {
        return self
    }
}

extension Data : DecodeType {
   func asDictionary() -> NSDictionary? {
      if let dic = try? JSONSerialization.jsonObject(with: self, options: .mutableContainers) {
        return dic as? NSDictionary
      }
    return nil
   }
}
複製代碼

修改協議Decodable協議,限定參數類型DecodeType協議:

protocol Decodable {
   static func parse(data : DecodeType)->Self?
}
複製代碼

修改LoginUser的解析方式:

extension LoginUser : Decodable{
    static func parse(data: DecodeType) -> LoginUser? {
        return LoginUser(data: data.asDictionary()!)
    }
}
複製代碼

JNLocalClientsend方法不須要在解析JSON,直接調用Parse解析:

if let data = try? String.init(contentsOfFile: fileURL!, encoding: .utf8) {
                let jsonData:Data = data.data(using: .utf8)!
                if let res = T.Response.parse(data: jsonData) {
                        handler(res)
                }else {
                        handler(nil)
                }
            }else {
                handler(nil)
            }
複製代碼

這樣用起來就更方便了。 回到剛纔的話題,有了JNLocalClient,咱們就能夠不受網絡的限制,單獨測試LoginRequestparse是否正常,以及以後的各個流程是否正常。

let request = LoginRequest(parameter: ["account":"19916721676","password":"123456"])
        JNLocalClient().send(request) { (loginUser) in
            XCTAssertNotNil(loginUser)
        }
複製代碼

這裏沒有使用任何第三方測試庫,也沒有運用網絡代理和運行時消息轉發,就能夠對請求進行測試。

解耦&可擴展

基於POP實現的代碼高度解耦,爲代碼的擴展提供相對寬鬆的可能性。在上面的例子中,咱們能夠僅僅實現發送請求的方法,在不影響請求定義和使用的狀況下更換了請求方式。這裏咱們使用手動解析賦值模型,咱們咱們徹底可使用第三方解析庫,好比:HandyJSON,來幫助咱們迅速構建模型類型。

相關文章
相關標籤/搜索