上一篇Swift面向協議編程(POP)中,咱們瞭解了POP
,以及POP
解決的問題,優勢和特性。本篇咱們咱們POP
來對網絡層封裝,體驗POP
帶來的解耦合,易於測試,強大的擴展性。編程
首先咱們須要知道,網絡層通常狀況下作的是從一個API
請求到JSON
數據,而後轉化爲一個可用的實例對象。 那麼咱們用一個登陸的例子來說解一下這個過程。 登陸接口: http://www.jihuabiao.net:8890/plan/freeuser/login
參數: account
,password
返回結構:json
{
loginUser = {
id = 2c93167b6cb7dd34016cc1a32802002a;
nickname = jensen;
phone = 199****1676;
};
}
複製代碼
首先新建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 }
}
複製代碼
host
和path
拼接而成GET
和POST
,本例使用POST
parameter
是請求的參數建立LoginRequest
實現JNRequest
bash
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
路徑method
爲POST
這個時候,咱們已經有了發請求的條件(路由,方法,參數)。下一步就須要發送請求了。咱們能夠爲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
,還進行數據的解析成,這樣作就沒法在不修改請求的狀況下更改解析的方式,增長耦合度,不利於測試。發送請求也是它的一部分,這樣請求的具體實現就和請求產生耦合,這也是不合理的...
鑑於上述實現存在一些問題,咱們着手重構代碼,解決上述存在的問題。 首先咱們先將send
從Request
中剝離出來,咱們須要一個單獨的類型來負責發送請求。基於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
對網絡層封裝的實戰。
如上述提到,咱們只是定義了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()!)
}
}
複製代碼
JNLocalClient
的send
方法不須要在解析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
,咱們就能夠不受網絡的限制,單獨測試LoginRequest
、 parse
是否正常,以及以後的各個流程是否正常。
let request = LoginRequest(parameter: ["account":"19916721676","password":"123456"])
JNLocalClient().send(request) { (loginUser) in
XCTAssertNotNil(loginUser)
}
複製代碼
這裏沒有使用任何第三方測試庫,也沒有運用網絡代理和運行時消息轉發,就能夠對請求進行測試。
基於POP
實現的代碼高度解耦,爲代碼的擴展提供相對寬鬆的可能性。在上面的例子中,咱們能夠僅僅實現發送請求的方法,在不影響請求定義和使用的狀況下更換了請求方式。這裏咱們使用手動解析賦值模型,咱們咱們徹底可使用第三方解析庫,好比:HandyJSON
,來幫助咱們迅速構建模型類型。