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)
}
複製代碼

以上就是對網絡封裝的抽象。固然,這可能還算不得很優雅的方式。我這裏也只是拋磚引玉,小夥伴們確定有更好的方式,感興趣的就來評論區交流吧。

相關文章
相關標籤/搜索