Swift面向协议编程-Alamofire网络请求

MQTT 协议核心角色

MQTT 协议主要有三大核心角色:发布者(Publisher)、Broker代理服务器(转发者)、订阅者(Subscriber)。其中消息的发布者和订阅者都是客户端角色,消息代理是服务器,消息发布者可以同时是订阅者。

1、 MQTT客户端

MQTT客户端身兼二职:既可以是发布者角色,又可以是订阅者角色。一个使用MQTT协议的应用程序或者设备就是一个MQTT 客户端,工作时它需要主动去连接到代理服务器,所以MQTT客户端的功能有:

主动与Broker 建立连接,主动断开Broker的连接。

作为发布者角色,发布给其他客户端订阅的Topic

作为订阅者,主动订阅其它客户端发布的Topic

退订之前订阅的Topic

清空服务端之前保留的Message

2、 MQTT服务器

MQTT通信必须依赖一个MQTT Broker,Broker(服务器)可以看出是MQTT网络的Hub,负责处理客户端的订阅逻辑并转发给其他订阅的客户端,如下图所示。

MQTT服务器又称为"消息代理"服务器(Broker),可以是一个应用程序或一台设备,它是位于消息发布者和订阅者之间,具有以下功能:

接受来自客户端的网络连接并建立通信链路
接收发布者的Topic并转发给订阅者
处理来自客户端的订阅和退订请求
向订阅的客户转发相应地Topic

-----------------------------------------------------------------------------------------分割线-----------------------------------------------------------------------------------------------

面向协议

先看代码:

protocol Run {
    var name: String { get }
    func run()
}

代码中定义名为Run协议,包含一个name属性,以及一个run方法的定义 所谓协议,就是属性的定义和方法的声明(注意这里只需要声明即可),而如果某个具体类型想要遵守一个协议,那它需要实现这个协议所定义的所有内容

POP就是通过协议扩展,协议继承和协议组合的方式来设计需要编写的代码。

首先在Swift中,值类型优先于类。然而,面向对象的概念不能很好地与结构体和枚举一起工作: 因为结构体和枚举不能够被继承。

再者,实际开发工程中,我们经常会遇到如下场景: 假设我们有一个ViewController,它继承自UIViewController,我们向其新添加一个方法 customMethod:

class ViewController: UIViewController {
    //新添加
    func customMethod() {
    }
}

这个时候我们有另外一个继承自UITableViewController的OtherViewController,同样也需要向其添加方法customMethod

class OtherViewController: UITableViewController {
    //新添加
    func customMethod() {
    }
}

这里就存在一个问题:很难在不同继承关系的类里共用代码。
我们的关注点customMethod位于两条继承链 UIViewController -> ViewCotroller 和 UIViewController -> UITableViewController -> AnotherViewController 的横切面上。面向对象是一种不错的抽象方式,但是肯定不是最好的方式。它无法描述两个不同事物具有某个相同特性这一点。在这里,特性的组合要比继承更贴切事物的本质。

总的来说,面向协议编程(POP) 带来的好处如下:

结构体、枚举等值类型也可以使用
以继承多个协议,弥补 swift 中类单继承的不足
增强代码的可扩展性,减少代码的冗余
让项目更加组件化,代码可读性更高
让无需的功能代码组成一个功能块,更便于单元测试。

使用pop解决上面的问题

protocol ex {
    func customMethod();
}

在实际类型遵守这个协议:

extension ViewController :ex {
    func customMethod() {
        //
    }
}
extension OtherViewController : ex {
    func customMethod() {
        //
    }
}

上述方式就是复制粘贴

而协议的扩展是可以在 extension ex 中为 customMethod 添加一个实现:

extension ex {
    func customMethod() {
        //
    }
}

协议的特性及使用

协议扩展:

1.提供协议方法的默认实现和协议属性的默认值,从而使它们成为可选;符合协议的类型可以提供自己的实现,也可以使用默认的实现。
2.添加协议中未声明的附加方法实现,并且实现协议的任何类型都可以使用到这些附加方法。这样就可以给遵循协议的类型添加特定的方法

protocol Entity {
    var name: String {get set}
    static func uid() -> String
}

extension Entity {
    static func uid() -> String {
        return UUID().uuidString
    }
}

struct Order: Entity {
    var name: String
    let uid: String = Order.uid()
}
let order = Order(name: "My Order")
print(order.uid)

协议继承

协议可以从其他协议继承,然后在它继承的需求之上添加功能,因此可以提供更细粒度和更灵活的设计。

protocol Persistable: Entity {
    func write(instance: Entity, to filePath: String)
    init?(by uid: String)
}

struct InMemoryEntity: Entity {
    var name: String
}

struct PersistableEntity: Persistable {
    var name: String
    func write(instance: Entity, to filePath: String) { // ...
    }  
    init?(by uid: String) {
        // try to load from the filesystem based on id
    }
}

协议的组合

类、结构体和枚举可以符合多个协议,它们可以采用多个协议的默认实现。这在概念上类似于多继承。这种组合的方式不仅比将所有需要的功能压缩到一个基类中更灵活,而且也适用于值类型。

struct MyEntity: Entity, Equatable, CustomStringConvertible {
    var name: String
    // Equatable
    public static func ==(lhs: MyEntity, rhs: MyEntity) -> Bool {
        return lhs.name == rhs.name
    }
    // CustomStringConvertible
    public var description: String {
        return "MyEntity: \(name)"
    }
}
let entity1 = MyEntity(name: "42")
print(entity1)
let entity2 = MyEntity(name: "42")
assert(entity1 == entity2, "Entities shall be equal")

网络请求

iOS开发中,一般App端网络请求都是通过一个API请求到JSON数据,自己转化为model然后刷新UI。

这里用一个腾讯地图的API来获取数据(主要他们数据规范,还一直可以使用)API 和参数拼接到一起是

https://apis.map.qq.com/ws/place/v1/suggestion/?region=北京&keyword=美食&key=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77

这个API返回的数据是


{
    "status":0,
    "message":"query ok",
    "count":100,
    "data":[{
        "id":"4062879599476274838",
        "title":"海底捞火锅(西单店)",
        "address":"北京市西城区西单北大街109号西单婚庆大楼7层",
        "category":"美食:火锅",
        "type":0,
        "location":{
            "lat":39.9139,
            "lng":116.3732
        },
        "adcode":110102,
        "province":"北京市",
        "city":"北京市",
        "district":"西城区"
    },
      ...
    ],
    "request_id":"1053741451029820730"
}

根据API返回的数据类型创建数据模型RestanurantTotal


import Foundation

struct RestanurantTotal {
    let status:Int
    let message:String
    var data:Array
    let count:Int
    let request_id:String
    
    init?(data: Any) {

        guard let total = data as? Dictionary else {
        return nil
    }
        
    guard let status = total["status"] as? Int else {
             return nil
         }
        self.status = status
        
        guard let message = total["message"] as? String else {
                 return nil
             }
            self.message = message
        
        guard let count = total["count"] as? Int else {
                 return nil
             }
            self.count = count
        
        guard let request_id = total["request_id"] as? String else {
                 return nil
             }
        self.request_id = request_id
        
        
        guard let restaurant = total["data"] as? Array> else {
                      return nil
        }
        
        let array:NSMutableArray = NSMutableArray.init()
        
        for index in 0..
}
    
}
    
    
struct Restaurant {

    let id : String
    let title: String
    let address: String
    let category: String
    let type: Int
    let location: LLocation
    let adcode: Int
    let province: String
    let district: String


    init?(info: Any) {

        guard let restaurant = info as? Dictionary else {
            return nil
        }
        guard let id = restaurant["id"] as? String else {
            return nil
        }
        guard let title = restaurant["title"] as? String else {
             return nil
         }
        guard let address = restaurant["address"] as? String else {
            return nil
        }
        guard let category = restaurant["category"] as? String else {
            return nil
        }
        guard let type = restaurant["type"] as? Int else {
            return nil
        }
        guard let adcode = restaurant["adcode"] as? Int else {
            return nil
        }
        guard let province = restaurant["province"] as? String else {
                  return nil
        }
        guard let district = restaurant["district"] as? String else {
                  return nil
        }
        guard let  location = restaurant["location"] as? Dictionary else {
                  return nil
        }

        self.id = id
        self.title = title
        self.address = address
        self.category = category
        self.type = type
        self.adcode = adcode
        self.province = province
        self.district = district
        self.location = LLocation.init(data: (location ))!
        
    }
}

struct  LLocation {
    let lat: Double
    let lng: Double
    
    init?(data: Any) {

        guard let location = data as? Dictionary else {
            return nil
        }
        guard let lat = location["lat"] as? Double else {
            return nil
        }

        guard let lng = location["lng"] as? Double else {
            return nil
        }
        self.lat = lat
        self.lng = lng
}
    
}

init中传入一个NSDictionary,创建一个RestanurantTotal实例。
如何使用POP的方式从URL请求到数据并生成对应的RestanurantTotal,是这里的重点。
我们知道Request是网络请求的入口,所以可以直接创建一个网络请求协议,网络请求需要知道路径,方法,参数等等。

enum HPHTTPMethod {
    case GET
    case POST
}

protocol HPRequest {
    var host : String {get}
    var path : String {get}
    var method : HPHTTPMethod {get}
    var parameter: [String: Any] { get }
}

请求地址由host和path拼接而成
method支持GET和POST,本例使用GET
parameter是请求的参数

struct TencentRequest: HPRequest {
    typealias Response = RestanurantTotal
    var host: String  {
        return "https://apis.map.qq.com/ws/place/v1/"
//        ?region=北京&keyword=美食&key=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77
    }
    var path: String {
        return "suggestion/"
    }
    let method: HPHTTPMethod = .GET
    var parameter: [String : Any]
}

设置host路径和path路径
指定method为GET

这个时候,我们已经有了发请求的条件(路由,方法,参数)。下一步就需要发送请求了。我们可以为HPRequest扩展发送请求的能力,这样可以让每一个请求都是用一样的方法发送的。

extension HPRequest {
    
    func sendRequest(hander:@escaping(RestanurantTotal)->Void) {
        //...
    }}

为HPRequest扩展sendRequest
逃逸闭包hander可将请求结果返回到外界

这里返回的是RestanurantTotal模型,这样的话sendRequest方法就只能支持这个餐馆请求。我们可以使用关联类型解决这个问题,使请求一般化.

protocol HPRequest {
   //.....
    associatedtype Response
}

struct RencentRequest: JNRequest {
    typealias Response = RestanurantTotal
    //....
}
extension HPRequest {
    func sendRequest(hander:@escaping(Response)->Void) {
    //...
    }
}

HPRequest协议中添加associatedtype Response
TencentRequest添加typealias Response = RestanurantTotal,执行返回类型为RestanurantTotal
HPRequest的扩展方法sendRequest的逃逸闭包将返回类型改为Response

sendRequest发送方法中,使用Alamofire发送网络请求

        let url = self.host + self.path
        Alamofire.request(url, method: HTTPMethod.get, parameters: self.parameter).responseJSON { (response) in
                    switch response.result {
                    print(result)
                }
          }

拼接url
调用Alamofire.request请求数据
使用JSON序列化器
获取到网络返回的JSON数据

现在还差最后一步,将返回的JSON数据转化为RestanurantTotal模型数据 我们为HPRequest协议添加方法

    func parse(data: NSDictionary) -> Response?

TencentRequest的扩展中实现parse

extension TencentRequest {
    
    func parse(data: NSDictionary) -> RestanurantTotal? {
        return RestanurantTotal(data:data)
    }
}

sendRequest中调用序列化解析

   func sendRequest(hander:@escaping(Response?)->Void) {
        //...
        let url = self.host + self.path
        Alamofire.request(url, method: HTTPMethod.get, 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 = TencentRequest(parameter: ["region":"北京","keyword":"美食","key":"YSJBZ-E7KWX-KJM4K-7Z6S7-TREBF-ILBJG"])
                    request.sendRequest { (Total) in
                        let total:RestanurantTotal = (Total as RestanurantTotal?)!
                        let resta:Restaurant = total.data.first! as! Restaurant
                        print("第一个地区的id是\(resta.id)")
                }

使用起来非常便捷,也能实现需求。但是这样的实现非常差劲。回头看看HPRequest的定义和扩展:

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

extension HPRequest {
    func sendRequest(hander:@escaping(Response?)->Void) {
     ...      
}

上面的实现主要问题在于Request管理的东西太多.Request该做的事情应该是定义请求入口,保存请求的信息和响应类型。而这里的Request保存host,还进行数据的解析成,这样做就无法在不修改请求的情况下更改解析的方式,增加耦合度,不利于测试。发送请求也是它的一部分,这样请求的具体实现就和请求产生耦合,这也是不合理的...

重构优化

鉴于上述问题,开始优化代码,先将send从Request中剥离出来。因此需要一个单独的类型负责发送请求。根据POP协议,定义以下协议

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

host不应该在Request中设置,我们将其移动到LHDataClient。清除请求中的host以及send。并定义LHAlamofireClient实现LHDataClient协议:

struct LHAlamofireClient {
   
    static let `default` = LHAlamofireClient()

   var host: String  {
       return "https://apis.map.qq.com/ws/place/v1/"
   }
   func send(_ r : T, handler: @escaping(T.Response?)->Void) {

           let url = self.host + r.path
    Alamofire.request(url, method:HTTPMethod.get, parameters: r.parameter).responseJSON { (response) in
               switch response.result {
               case .success(let data):
                if let dic = data as? NSDictionary {
                    if let res = T.Response.parse(data: dic) {
                        handler(res)
                    }else {
                        handler(nil)
                    }
                }else {
                    handler(nil)
                }

               case .failure:
                   handler(nil)
               }
           }
   }
}


目前已经将发送请求和请求本身分离开,我们定义了LHDataClient协议,这里实现了LHAlamofireClient,使用Alamofire发送请求。对象的解析不应该由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
}

为Model类扩展协议方法

extension RestanurantTotal : Decodable {
   static func parse(data: NSDictionary) -> RestanurantTotal? {
       return RestanurantTotal(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)
                    }

创建单例

    static let `default` = LHAlamofireClient()

外部调用

 let request = LHlhRequest(parameter: ["region":"北京","keyword":"美食","key":"JBZ-E7KWX-KJM4K-7Z6S7-TREBF-ILBJG"])
                LHAlamofireClient.default.send(request) {(Total) in
                           print(Total!)
                    
                    let total:RestanurantTotal = (Total as RestanurantTotal?)!
                                    let resta:Restaurant = total.data.first! as! Restaurant
                                    print("第一个地区的id是\(resta.id)")
                }
         
                        
                
            })

如果需要创建其他的请求,可以使用和LHlhRequest相似的方式,为网络层添加其他的API请求,只需要定义请求所必要的内容,而不用担心会触及网络方面的具体实现。

易于测试

准备一个response.json的文件,内容是

{
    "status":0,
    "message":"query ok",
    "count":100,
    "data":[{
        "id":"4412846406955100612",
        "title":"鲜鱼口老字号美食街",
        "address":"北京市东城区前门东路附近",
        "category":"美食:中餐厅:其它中餐厅",
        "type":0,
        "location":{
            "lat":39.89605,
            "lng":116.39991
        },
        "adcode":110101,
        "province":"北京市",
        "city":"北京市",
        "district":"东城区"
    },
    {
        "id":"12500720047859529446",
        "title":"初色海鲜自助火锅",
        "address":"北京市丰台区万丰路302号",
        "category":"美食:海鲜",
        "type":0,
        "location":{
            "lat":39.870803,
            "lng":116.293982
        },
        "adcode":110106,
        "province":"北京市",
        "city":"北京市",
        "district":"丰台区"
    },
    {
        "id":"8301295427127788926",
        "title":"王府井小吃街",
        "address":"北京市东城区王府井大街与大纱帽胡同交叉口西北角",
        "category":"美食:小吃快餐",
        "type":0,
        "location":{
            "lat":39.910895,
            "lng":116.410904
        },
        "adcode":110101,
        "province":"北京市",
        "city":"北京市",
        "district":"东城区"
    },
    {
        "id":"11385228330756553756",
        "title":"新干线美食一条街",
        "address":"北京市朝阳区霄云路35号三元桥",
        "category":"美食:中餐厅:其它中餐厅",
        "type":0,
        "location":{
            "lat":39.957371,
            "lng":116.460884
        },
        "adcode":110105,
        "province":"北京市",
        "city":"北京市",
        "district":"朝阳区"
    },
    {
        "id":"2151925127924188045",
        "title":"鼎好美食广场",
        "address":"北京市海淀区中关村日月光·鼎好大厦B座",
        "category":"美食:小吃快餐",
        "type":0,
        "location":{
            "lat":39.983855,
            "lng":116.314364
        },
        "adcode":110108,
        "province":"北京市",
        "city":"北京市",
        "district":"海淀区"
    },
    {
        "id":"6120575816560518252",
        "title":"全聚德烤鸭店(天安门店)",
        "address":"北京市东城区东交民巷44号",
        "category":"美食:其它美食",
        "type":0,
        "location":{
            "lat":39.90147,
            "lng":116.40007
        },
        "adcode":110101,
        "province":"北京市",
        "city":"北京市",
        "district":"东城区"
    },
    {
        "id":"16827131455368440528",
        "title":"上可味美食广场",
        "address":"北京市丰台区南四环西路188号",
        "category":"美食:小吃快餐",
        "type":0,
        "location":{
            "lat":39.824105,
            "lng":116.283744
        },
        "adcode":110106,
        "province":"北京市",
        "city":"北京市",
        "district":"丰台区"
    },
    {
        "id":"12511480423602913915",
        "title":"可味美食城(三里屯店)",
        "address":"北京市朝阳区工人体育场北路8号院三里屯SOHO6号商场B1层",
        "category":"美食:小吃快餐",
        "type":0,
        "location":{
            "lat":39.93167,
            "lng":116.45363
        },
        "adcode":110105,
        "province":"北京市",
        "city":"北京市",
        "district":"朝阳区"
    },
    {
        "id":"8119722848339667861",
        "title":"美食广场食字街区(北京市百货大楼)",
        "address":"北京市东城区王府井大街255号北京市百货大楼F7",
        "category":"美食:小吃快餐",
        "type":0,
        "location":{
            "lat":39.914084001,
            "lng":116.410407448
        },
        "adcode":110101,
        "province":"北京市",
        "city":"北京市",
        "district":"东城区"
    },
    {
        "id":"7632255329310100195",
        "title":"西单明珠市场美食广场",
        "address":"北京市西城区横二条59号西单明珠市场8层",
        "category":"美食:中餐厅:其它中餐厅",
        "type":0,
        "location":{
            "lat":39.9103,
            "lng":116.37618
        },
        "adcode":110102,
        "province":"北京市",
        "city":"北京市",
        "district":"西城区"
    }],
    "request_id":"658616875259013526"
}

步骤,创建一个类型LHLocalClient,实现LHDataClient协议:

struct LHLocalClient {
    
    var host: String  {
        return ""
    }
    func send(_ r : T, handler: @escaping(T.Response?)->Void) {
        
        switch r.path {
        case "suggestion/":
            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)
//                }
//            }
            
            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)
            }
        default:
            handler(nil)
        }
    }
}

检查输入请求的path属性,根据path不同,从bundle中读取预先设定的文件数据。
对返回的结果做JSON解析,然后调用Response的parse解析
调用handler返回数据到外界。
如果我们需要增加其他请求的测试,可以添加新的case项

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

所以在LHLocalClient中我们需要自己解析成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?
}

修改RestanurantTotal的解析方式:

extension RestanurantTotal : Decodable {
   static func parse(data: DecodeType) -> RestanurantTotal? {
       return RestanurantTotal(data: data.asDictionary()!)
   }
}

LHLocalClient的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)
            }

这样用起来就更方便了。 回到刚才的话题,有了LHLocalClient,我们就可以不受网络的限制,单独测试LoginRequest、 parse是否正常,以及之后的各个流程是否正常。

解耦&可扩展

基于POP实现的代码高度解耦,为代码的扩展提供相对宽松的可能性。在上面的例子中,我们可以仅仅实现发送请求的方法,在不影响请求定义和使用的情况下更换了请求方式。这里我们使用手动解析赋值模型,我们我们完全可以使用第三方解析库,HandyJSON,来帮助我们迅速构建模型类型。

pod里面添加代码

pod 'HandyJSON'

步骤参考:https://juejin.im/post/5d6a1bad518825391623e64b
代码拉取地址:https://gitee.com/xgkp/SwiftNetRequest.git

你可能感兴趣的:(Swift面向协议编程-Alamofire网络请求)