Swift之面向协议编程POP

1,892 阅读8分钟

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

一、面向对象OOP

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

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)的核心思想:封装与继承。

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

比如有一个下面这样的模型:

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 跑步运动员有跑步的能力。

那么现在有一个人,他即是教师又是一个跑步运动员该如何处理呢?

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

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

由于面向对象OOP有这么多缺陷,所以,就有了面向协议POP。

二、面向协议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既有教师和跑步运动员的能力。

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

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

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

在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)
}

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

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

虽然上面的封装已经有很多优点了,但是,总感觉有美中不足的地方。

首先,继承自 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)
}

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