在Swift发布之后,就常常听大神们提及面向协议编程POP。听得多了,天然心生向往,今天就来了解一下什么是POP。编程
目前,大多数开发仍然使用的是面向对象的方式。咱们都知道面向对象的三大特性:封装、继承、多态。 举个栗子🌰: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
复制代码
在上面👆的栗子中,BOTiger
和 BOAnimal
共享了一部分代码,这部分代码被封装到了父类 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
BOTeacher
的子类复制一份 run
的代码,让其具备跑步运动员的能力。但这是坏代码的开始,开发者应该避免这样的方式。BOPerson
添加 run
的能力。可是这样就会使其余继承于 BOPerson
的类也具备 run
的能力,但可能它并不须要这样的能力。run
能力的对象,好比给 BOTeacher
新增一个副业。可是会引入额外的依赖关系,也不是很好的解决方式。因为面向对象OOP有这么多缺陷,因此,就有了面向协议POP。ide
仍是上面 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
复制代码
将 BOPerson
、BOTeacher
、BORuner
都改成协议。而具体的类型 PersonA
将继承于 BOTeacher
和 BORuner
。这样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)
}
复制代码
这样作有什么好处呢? 一、各功能模块的网络请求能够相互独立。包括主机的地址、请求的路由等均可以自定义,保证了网络请求的灵活性。 二、网络请求统一发送。再也不须要对每一个功能模块都重写一次网络请求,减小了重复的操做。 三、对外提供定制接口。如提供了数据解析的接口,可让针对各个功能模块作不一样的处理。
虽然上面的封装已经有不少优势了,可是,总感受有美中不足的地方。
首先,继承自 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)
}
复制代码
以上就是对网络封装的抽象。固然,这可能还算不得很优雅的方式。我这里也只是抛砖引玉,小伙伴们确定有更好的方式,感兴趣的就来评论区交流吧。