在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)
}
複製代碼
以上就是對網絡封裝的抽象。固然,這可能還算不得很優雅的方式。我這裏也只是拋磚引玉,小夥伴們確定有更好的方式,感興趣的就來評論區交流吧。