Swift之面向协议编程POP

在Swift发布之后,就常常听大神们提及面向协议编程POP。听得多了,天然心生向往,今天就来了解一下什么是POP。编程

1、面向对象OOP

目前,大多数开发仍然使用的是面向对象的方式。咱们都知道面向对象的三大特性:封装、继承、多态。 举个栗子🌰:swift

class BOAnimal {
    
    // 默认动物有2条腿
    var leg: Int { return 2 }
    
    // 默认动物都要吃食物
    func eat() {
        print("eat food.")
    }
    
    // 默认动物均可以奔跑
    func run() {
        print("run with \(leg) legs")
    }
}

class BOTiger: BOAnimal {
    
    // 老虎有4条腿
    override var leg: Int { return 4 }
    
    // 老虎吃肉
    override func eat() {
        print("eat meat.")
    }
}

let tiger = BOTiger()
tiger.eat() // eat meat.
tiger.run() // run with 4 legs
复制代码

在上面👆的栗子中,BOTigerBOAnimal 共享了一部分代码,这部分代码被封装到了父类 BOAnimal 中,除了 BOTiger 这个子类以外,其他的 BOAnimal 子类也可使用这部分代码。这就是面向对象(OOP)的核心思想:封装与继承。api

虽然咱们在开发过程当中努力使用这套抽象和继承的方式建模,可是实际的事物每每是一系列特质的组合,而不只仅是一脉相承逐渐扩展的方式构建的。网络

好比有一个下面这样的模型:闭包

class BOPerson {
    
    var name:String?
}

class BOTeacher: BOPerson {
    
    func teach() {
        
        print("\(name ?? "") teach student")
    }
}

class BORuner: BOPerson {
    
    func run() {
        
        print("\(name ?? "") run fast")
    }
}
复制代码

基类 BOPerson 表示一我的,每一个人都有一个名字。子类 BOTeacher 教师有一个教书的能力。子类 BORuner 跑步运动员有跑步的能力。架构

那么如今有一我的,他便是教师又是一个跑步运动员该如何处理呢?app

那么可能会有以下几种解决方案:async

  • 一、Copy & Paste:给继承于 BOTeacher 的子类复制一份 run 的代码,让其具备跑步运动员的能力。但这是坏代码的开始,开发者应该避免这样的方式。
  • 二、基类:给 BOPerson 添加 run 的能力。可是这样就会使其余继承于 BOPerson 的类也具备 run 的能力,但可能它并不须要这样的能力。
  • 三、依赖注入:经过外界引入带有 run 能力的对象,好比给 BOTeacher 新增一个副业。可是会引入额外的依赖关系,也不是很好的解决方式。
  • 四、多继承:可是Swift并不支持多继承。即便支持多继承,也会带来另外一个著名的OOP问题:菱形缺陷。即若是继承的两个类都有一样的方法,子类就很难肯定继承的究竟是哪一个父类的方法。

因为面向对象OOP有这么多缺陷,因此,就有了面向协议POP。ide

2、面向协议POP

仍是上面 BOPerson 的栗子:函数

protocol BOPerson {
    var name: String { get }
}

protocol BOTeacher {
    
    func teach()
}

extension BOTeacher {
 
    func teach() {
        print("teach student")
    }
}

protocol BORuner {
    
    func run()
}

extension BORuner {
    
    func run() {
        
        print("run fast")
    }
}

class PersonA: BOTeacher, BORuner {
    
    let name: String = "personA"
}

let personA = PersonA()
personA.teach() // teach student
personA.run()   // run fast
复制代码

BOPersonBOTeacherBORuner都改成协议。而具体的类型 PersonA 将继承于 BOTeacherBORuner。这样personA既有教师和跑步运动员的能力。

总结:面向协议编程就是将对象所拥有的能力抽象为协议。经过拼装不一样的协议组合,让对象拥有不一样的能力组合。

最后,还可使用协议扩展给协议添加默认实现。

3、面向协议实战--网络层封装

在Swift项目开发中,小伙伴们可能会使用MVVM架构,而其中网络请求通常会放在ViewModel中。而在网络层,也会有一些封装,封装方法不少,各种封装方法的优缺也不一而足。

那么如何使用面向协议来封装网络请求呢?让咱们一步步来实现。

// 网络请求方式
enum HttpMethod: String {
    case POST
    case GET
}

protocol BORequest {
    
    // 请求地址
    var host: String { get }
    
    // 请求路由
    var path: String { get }
    
    // 请求方式
    var method: HttpMethod { get }
    
    // 请求参数
    var pramars: [String : Any]  { get }
}
复制代码

如上代码中,定义协议 BORequest 包含网络请求须要的地址、路由、请求方式、请求参数属性。

再给 BORequest 协议一个默认实现 request

extension BORequest {

    // 发送请求的方法
    func request(handler: @escaping () -> Void) {
        
        // 请求网络 -> 序列化 -> Model
    }
}
复制代码

request 函数的做用是发送网络请求,而且将返回的数据序列化为模型Model,并返回。因此逃逸闭包应该有一个参数,可是这里有个问题,若是指定一个类型,那么就只能返回指定类型的数据了。若是返回Any类型,又不利于序列化。

这里就显示出泛型的便利了,这里可使用泛型做为参数类型,即解决了序列化的问题,又让 request 请求数据灵活多变。

而且为了序列化能够灵活定制,因此也应该给提供一个接口给外界实现。整理以后的代码以下:

protocol BORequest {
    
    // 请求地址
    var host: String { get }
    
    // 请求路由
    var path: String { get }
    
    // 请求方式
    var method: HttpMethod { get }
    
    // 请求参数
    var pramars: [String : Any]  { get }
    
    associatedtype Response
    
    // 序列化方法
    func parse(data: Data) -> Response?
}

extension BORequest {

    // 发送请求的方法
    func request(handler: @escaping (_ response: Response?) -> Void) {
        
        // 请求网络 -> 序列化 -> Model
        
        let url = URL(string: host.appending(path))
        
        guard let requestUrl = url else { return; }
        
        var request = URLRequest.init(url: requestUrl)
        
        request.httpMethod = method.rawValue
        
        let task = URLSession.shared.dataTask(with: request) {
            (data, response, error) in
            
            if let data = data, let resp = self.parse(data: data) {
                DispatchQueue.main.async {
                    
                    handler(resp)
                }
            }
        }
        
        task.resume()
    }
}
复制代码

BORequest 协议就基本完成了,那么该如何使用呢?

struct BOLoginRequest: BORequest {
    
    var name: String
    
    let host: String = "https://xxxx.com"
    
    let path: String = "/login_api"
    
    let method: HttpMethod = .POST
    
    var pramars: [String : Any] {
        
        return ["username": name]
    }
    
    typealias Response = BOLoginModel
    
    func parse(data: Data) -> BOLoginModel? {
        
        // 为了简化这里就直接使用伪代码了
        
        return BOLoginModel(id: "1", username: "BO", token: "xxx")
    }
}

struct BOLoginModel {
    
    var id: String
    
    var username: String
    
    var token: String
}
复制代码

定义一个结构体 BOLoginRequest 继承自 BORequest 做为登陆模块网络请求的具体实现者。具体的请求地址以及解析,这里使用了伪代码,小伙伴们能够自行实现。

因为登陆的网络请求还须要一些参数,因此添加一个参数 name,这个 name 能够从外面传递,保证了参数的灵活性。

定义好以后,就能够网络请求了。

let loginRequest = BOLoginRequest(name: "BO")

loginRequest.request { (loginModel) in
    
    print(loginModel)
}
复制代码

这样作有什么好处呢? 一、各功能模块的网络请求能够相互独立。包括主机的地址、请求的路由等均可以自定义,保证了网络请求的灵活性。 二、网络请求统一发送。再也不须要对每一个功能模块都重写一次网络请求,减小了重复的操做。 三、对外提供定制接口。如提供了数据解析的接口,可让针对各个功能模块作不一样的处理。

4、面向协议实战--网络层封装改进

虽然上面的封装已经有不少优势了,可是,总感受有美中不足的地方。

首先,继承自 BORequest 的类都有一个host属性须要赋值,可是实际开发中,host基本只有一个,不会轻易改变。

其次,让 BORequest 来处理序列化的事情,也不是一种好的方式,会让各部分耦合严重。

还有,让继承自 BORequest 的类直接发起网络请求也不利于管理。因此还需对网络层进行封装。

首先,咱们抽象出一个管理类协议 BOClientProtocol 来提供 host,让管理类来管理请求的主机地址。同时,剥离 BORequest 的请求网络的能力,让 BOClientProtocol 来提供请求网络的能力,统一管理。

因为请求的路由和参数仍是须要 BORequest 来提供,因此,request 函数须要多一个参数。

protocol BOClientProtocol {
    
    // 请求地址
    var host: String { get }
    
    func request<T: BORequest>(_ r: T, handler: @escaping (T.Response?) -> Void)
}
复制代码

因为 BORequest 仅做为参数,并且序列化也不该该由 BORequest 提供,因此将序列化抽象为一个协议 BODecodable

protocol BODecodable {
    
    // 序列化->模型
    static func parse(data: Data) -> Self?
}
复制代码

因此,BORequest 被精简为:

protocol BORequest {
    
    // 请求路由
    var path: String { get }
    
    // 请求方式
    var method: HttpMethod { get }
    
    // 请求参数
    var pramars: [String : Any]  { get }
    
    associatedtype Response: BODecodable
}
复制代码

以上三个协议就是网络请求抽象出的三个抽象协议:请求管理者BOClientProtocol、请求参数BORequest、返回模型BODecodable

如此抽象封装后,各个抽象的功能单一明确,耦合度低,逻辑清晰。

再对三个协议进行实现:

class BOClient: BOClientProtocol {
    
    // 单例
    static let manager = BOClient()
    
    let host: String = "https://xxx.com"
    
    func request<T>(_ r: T, handler: @escaping (T.Response?) -> Void) where T : BORequest {
        
        // 请求网络 -> 序列化 -> Model
        
        let url = URL(string: host.appending(r.path))
        
        guard let requestUrl = url else { return; }
        
        var request = URLRequest.init(url: requestUrl)
        
        request.httpMethod = r.method.rawValue
        
        let task = URLSession.shared.dataTask(with: request) {
            (data, response, error) in
            
            if let data = data, let resp = T.Response.parse(data: data) {
                DispatchQueue.main.async {
                    
                    handler(resp)
                }
            }
        }
        
        task.resume()
    }
}
复制代码
struct BOLoginRequest: BORequest {
    
    var name: String
    
    let path: String = "/login_api"
    
    let method: HttpMethod = .POST
    
    var pramars: [String : Any] {
        
        return ["username": name]
    }
    
    typealias Response = BOLoginModel
}
复制代码
struct BOLoginModel {
    
    var id: String
    
    var username: String
    
    var token: String
}

extension BOLoginModel: BODecodable {
    static func parse(data: Data) -> BOLoginModel? {
        
        // 为了简化这里就直接使用伪代码了
        return BOLoginModel(id: "1", username: "BO", token: "xxx")
    }
}
复制代码

实现以后就能够很方便的使用了。

let loginRequest = BOLoginRequest(name: "BO")

BOClient.manager.request(loginRequest) { (response) in
    
    print(response)
}
复制代码

以上就是对网络封装的抽象。固然,这可能还算不得很优雅的方式。我这里也只是抛砖引玉,小伙伴们确定有更好的方式,感兴趣的就来评论区交流吧。

相关文章
相关标签/搜索