在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
在上面👆的栗子中,BOTiger
和 BOAnimal
共享了一部分代码,这部分代码被封装到了父类 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
将 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)
}
这样做有什么好处呢? 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)
}
以上就是对网络封装的抽象。当然,这可能还算不得很优雅的方式。我这里也只是抛砖引玉,小伙伴们肯定有更好的方式,感兴趣的就来评论区交流吧。