原創(chuàng):知識(shí)點(diǎn)總結(jié)性文章
創(chuàng)作不易,請(qǐng)珍惜,之后會(huì)持續(xù)更新,不斷完善
個(gè)人比較喜歡做筆記和寫總結(jié),畢竟好記性不如爛筆頭哈哈,這些文章記錄了我的IOS成長(zhǎng)歷程,希望能與大家一起進(jìn)步
溫馨提示:由于簡(jiǎn)書不支持目錄跳轉(zhuǎn),大家可通過(guò)command + F 輸入目錄標(biāo)題后迅速尋找到你所需要的內(nèi)容
目錄
- 一、POP面向協(xié)議編程
- 1、POP 面向協(xié)議編程相比面向?qū)ο缶幊痰膬?yōu)勢(shì)
- 2、使用POP進(jìn)行網(wǎng)絡(luò)請(qǐng)求
- 3、總結(jié)與解惑
- 二、初識(shí)Moya
- 1、Moya的簡(jiǎn)介
- 2、豆瓣范例
- 3、訂單范例
- 4、登錄范例
- Demo
- 參考文獻(xiàn)
一、POP面向協(xié)議編程
1、POP 面向協(xié)議編程相比面向?qū)ο缶幊痰膬?yōu)勢(shì)
a、橫切關(guān)注點(diǎn)問(wèn)題
指的是我們很難在不同繼承關(guān)系的類里共用代碼。想要解決這個(gè)問(wèn)題,我們有幾個(gè)方案。
Copy & Paste
這是一個(gè)比較糟糕的解決方案,但是演講現(xiàn)場(chǎng)還是有不少朋友選擇了這個(gè)方案,特別是在工期很緊,無(wú)暇優(yōu)化的情況下。這誠(chéng)然可以理解,但是這也是壞代碼的開(kāi)頭。我們應(yīng)該盡量避免這種做法。
引入 BaseViewController
在一個(gè)繼承自 UIViewController 的 BaseViewController 上添加需要共享的代碼,或者干脆在 UIViewController 上添加 extension??雌饋?lái)這是一個(gè)稍微靠譜的做法,但是如果不斷這么做,會(huì)讓所謂的 Base 很快變成垃圾堆。職責(zé)不明確,任何東西都能扔進(jìn) Base,你完全不知道哪些類走了 Base,而這個(gè)“超級(jí)類”對(duì)代碼的影響也會(huì)不可預(yù)估。
依賴注入
通過(guò)外界傳入一個(gè)帶有 myMethod 的對(duì)象,用新的類型來(lái)提供這個(gè)功能。這是一個(gè)稍好的方式,但是引入額外的依賴關(guān)系,可能也是我們不太愿意看到的。
面向協(xié)議
現(xiàn)在通過(guò)面向協(xié)議的方式,任何遵循協(xié)議的對(duì)象都可以使用協(xié)議中的方法和屬性,比如只有對(duì)象遵守了下面代碼中的PersonProtocl協(xié)議就可以使用 name 屬性以及sayHello()方法。
b、POP 解決橫切關(guān)注點(diǎn)
? 提供聲明
protocol PersonProtocl
{
// 協(xié)議屬性
var name: String {get}
// 協(xié)議方法
func sayHello()
}
通過(guò)結(jié)構(gòu)體來(lái)實(shí)現(xiàn)協(xié)議
struct Teacher: PersonProtocl
{
var name: String
func sayHello()
{
print("同學(xué)們好,請(qǐng)把周末的作業(yè)交上來(lái)")
}
}
struct Student: PersonProtocl
{
var name: String
func sayHello()
{
print("老師你好,我作業(yè)放在家里忘帶了")
}
}
進(jìn)行調(diào)用
override func viewDidLoad()
{
super.viewDidLoad()
let teacher = Teacher(name: "蔣紅")
let student = Student(name: "謝佳培")
teacher.sayHello()
student.sayHello()
}
輸出結(jié)果為
同學(xué)們好,請(qǐng)把周末的作業(yè)交上來(lái)
老師你好,我作業(yè)放在家里忘帶了
? 擴(kuò)展實(shí)現(xiàn)
但是仍然存在一個(gè)很大的問(wèn)題,那就是協(xié)議里的方法和屬性缺乏具體的實(shí)現(xiàn)。如果只是提供聲明,那意味著我們還需要在每一個(gè)類里面都實(shí)現(xiàn)一遍,那協(xié)議就顯得比較雞肋了,而且有很多時(shí)候這些方法是共有的,不需要太多的特定實(shí)現(xiàn)。這時(shí)候就需要對(duì)協(xié)議提供默認(rèn)實(shí)現(xiàn)的協(xié)議擴(kuò)展閃亮登場(chǎng)了。
extension PersonProtocl
{
func sayHello()
{
print("hello! boy")
}
}
對(duì)其進(jìn)行調(diào)用
class UsePop: UIViewController, PersonProtocl
{
var name: String = ""
override func viewDidLoad()
{
super.viewDidLoad()
sayHello()
}
}
輸出結(jié)果為
hello! boy
c、POP 解決動(dòng)態(tài)派發(fā)安全性
Objective-C 恰如其名,是一門典型的 OOP 語(yǔ)言,同時(shí)它繼承了 Small Talk 的消息發(fā)送機(jī)制。這套機(jī)制十分靈活,是 OC 的基礎(chǔ)思想,但是有時(shí)候相對(duì)危險(xiǎn)??紤]下面的代碼:
ViewController *v1 = ...
[v1 myMethod];
AnotherViewController *v2 = ...
[v2 myMethod];
NSArray *array = @[v1, v2];
for (id obj in array)
{
[obj myMethod];
}
我們?nèi)绻?ViewController 和 AnotherViewController 中都實(shí)現(xiàn)了 myMethod 的話,這段代碼是沒(méi)有問(wèn)題的。myMethod 將會(huì)被動(dòng)態(tài)發(fā)送給 array 中的 v1 和 v2。但是,要是我們有一個(gè)沒(méi)有實(shí)現(xiàn) myMethod 的類型,會(huì)如何呢?
NSObject *v3 = [NSObject new]
// v3 沒(méi)有實(shí)現(xiàn) `myMethod`
NSArray *array = @[v1, v2, v3];
for (id obj in array)
{
[obj myMethod];
}
編譯依然可以通過(guò),但是顯然,程序?qū)⒃谶\(yùn)行時(shí)崩潰。Objective-C 是不安全的,編譯器默認(rèn)你知道某個(gè)方法確實(shí)有實(shí)現(xiàn),這是消息發(fā)送的靈活性所必須付出的代價(jià)。而在 app 開(kāi)發(fā)看來(lái),用可能的崩潰來(lái)?yè)Q取靈活性,顯然這個(gè)代價(jià)太大了。雖然這不是OOP 范式的問(wèn)題,但它確實(shí)在 Objective-C 時(shí)代給我們帶來(lái)了切膚之痛。
Runtime error: unrecognized selector sent to instance blabla
與之相對(duì),對(duì)于沒(méi)有實(shí)現(xiàn) Protocl 提供的屬性和方法的對(duì)象,編譯器將進(jìn)行錯(cuò)誤提示,因此更加安全。
Type 'Teacher' does not conform to protocol 'PersonProtocl' Do you want to add protocol stubs?
d、POP 解決菱形缺陷
繼承中存在的一個(gè)重要問(wèn)題是菱形缺陷,也就是子類無(wú)法確定使用哪個(gè)父類的方法。在協(xié)議的對(duì)應(yīng)方面,這個(gè)問(wèn)題依然存在,因?yàn)槎鄠€(gè)協(xié)議可能存在相同的協(xié)議屬性、協(xié)議方法,遵循者也是無(wú)法確定使用的是哪個(gè)協(xié)議中的方法,所以我們?cè)陂_(kāi)發(fā)中一定要盡量規(guī)避多個(gè)協(xié)議中的同名問(wèn)題。
protocol AnimalProtocl
{
// 協(xié)議屬性
var name: String {get}
// 協(xié)議方法
func sayHello()
func canNotThink()
}
遵守協(xié)議
struct Teacher: PersonProtocl, AnimalProtocl
{
var name: String
func sayHello()
{
print("同學(xué)們好,請(qǐng)把周末的作業(yè)交上來(lái)")
}
func canNotThink()
{
print("動(dòng)物無(wú)法思考,僅僅憑借生存本能行動(dòng)")
}
}
進(jìn)行調(diào)用
func solveProblem()
{
let teacher = Teacher(name: "蔣紅")
let student = Student(name: "謝佳培")
teacher.sayHello()
student.sayHello()
teacher.canNotThink()
}
輸出結(jié)果
同學(xué)們好,請(qǐng)把周末的作業(yè)交上來(lái)
老師你好,我作業(yè)放在家里忘帶了
動(dòng)物無(wú)法思考,僅僅憑借生存本能行動(dòng)
如果我們?yōu)槠渲械哪硞€(gè)協(xié)議進(jìn)行了擴(kuò)展,在其中提供了默認(rèn)的 name 實(shí)現(xiàn),這樣的編譯是可以通過(guò)的,雖然 Teacher 中沒(méi)有定義 name,但是通過(guò) AnimalProtocl 的 name,Teacher 依然可以遵守 PersonProtocl。
extension AnimalProtocl
{
var name: String { return "another default name" }
}
struct Teacher: PersonProtocl, AnimalProtocl
{
// let name: String
}
不過(guò),當(dāng) PersonProtocl 和 AnimalProtocl 都有 name 的協(xié)議擴(kuò)展的話,就無(wú)法編譯了。這種情況下,Teacher 無(wú)法確定要使用哪個(gè)協(xié)議擴(kuò)展中 name 的定義。在同時(shí)實(shí)現(xiàn)兩個(gè)含有同名元素的協(xié)議,并且它們都提供了默認(rèn)擴(kuò)展時(shí),我們需要在具體的類型中明確地提供實(shí)現(xiàn)。這里我們將 Teacher 中的 name 進(jìn)行實(shí)現(xiàn)就可以了。
extension PersonProtocl
{
var name: String { return "default name" }
}
extension AnimalProtocl
{
var name: String { return "another default name" }
}
struct Teacher: PersonProtocl, AnimalProtocl
{
let name: String
}
let teacher = Teacher(name: "蔣紅")
2、使用POP進(jìn)行網(wǎng)絡(luò)請(qǐng)求
a、直接在ViewController (代表應(yīng)用層) 進(jìn)行網(wǎng)絡(luò)請(qǐng)求
- 應(yīng)用層與網(wǎng)絡(luò)層耦合在一起,但應(yīng)用層其實(shí)根本不應(yīng)該關(guān)心網(wǎng)絡(luò)請(qǐng)求的方法、接口、參數(shù)
- 到處嵌套,可復(fù)用性特別低
class StudentAndTeacher: UIViewController
{
AF.request("http://127.0.0.1:5000/pythonJson/")
.validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.responseData
{ response in
switch response.result
{
case .success:
print(response)
//let _ = LoginClient.json(data: response.data)
case .failure(let error):
print(error)
}
}
}
b、提供信息能力者
? 通過(guò)面向協(xié)議的方式給 PersonRequest 賦予網(wǎng)絡(luò)請(qǐng)求的能力(能夠提供網(wǎng)絡(luò)請(qǐng)求需要的各種屬性)
// 請(qǐng)求協(xié)議
protocol Requestable
{
// 請(qǐng)求路徑
var path: String { get }
// 請(qǐng)求方法
var method: HTTPMethod { get }
// 請(qǐng)求參數(shù)
var parameter: [String: Any] { get }
// 遵守解碼協(xié)議的關(guān)聯(lián)類型
// 通過(guò)在 Requestable 協(xié)議中添加一個(gè)關(guān)聯(lián)類型,我們可以將回調(diào)參數(shù)進(jìn)行抽象
associatedtype Response: DecodableProtocol
}
? 遵守請(qǐng)求協(xié)議
struct PersonRequest: Requestable
{
// 相應(yīng)地添加類型定義,以滿足協(xié)議,默認(rèn)使用的數(shù)據(jù)模型是 Person
typealias Response = Person
// 未定義初始值的 name 屬性
let name: String
// 將 host 和 path 拼接起來(lái)可以得到我們需要請(qǐng)求的 API 地址
var path: String
{
return "/users/\(name)"
}
// 在我們的例子中只會(huì)使用到 GET 請(qǐng)求
let method: HTTPMethod = .GET
// 因?yàn)檎?qǐng)求的參數(shù)用戶名 name 會(huì)通過(guò) URL 進(jìn)行傳遞,所以 parameter 是一個(gè)空字典就足夠了
let parameter: [String: Any] = [:]
}
c、網(wǎng)絡(luò)請(qǐng)求能力者
? HTTPMethod 提供本模塊 PersonRequest 需要的請(qǐng)求方法枚舉
enum HTTPMethod: String
{
case GET
case POST
}
? 客戶端協(xié)議:提供基地址屬性和發(fā)送請(qǐng)求方法
-
T是遵守請(qǐng)求協(xié)議的范型,request是請(qǐng)求,handler是請(qǐng)求完成后的回調(diào)閉包,Response是遵守解碼協(xié)議的關(guān)聯(lián)類型 - 定義了可逃逸的閉包
(T.Response?) -> Void。在請(qǐng)求完成后,我們調(diào)用這個(gè)handler方法來(lái)通知調(diào)用者請(qǐng)求是否完成,如果一切正常,則將一個(gè)數(shù)據(jù)模型Person實(shí)例傳回,否則傳回nil - 我們想要發(fā)送請(qǐng)求的
send方法對(duì)于所有的Request都通用,所以顯然回調(diào)的參數(shù)類型不能是數(shù)據(jù)模型Person - 因?yàn)?
Requestable是含有關(guān)聯(lián)類型的協(xié)議,所以它并不能作為獨(dú)立的類型來(lái)使用,我們只能夠?qū)⑺鳛轭愋图s束,來(lái)限制輸入?yún)?shù)request - 除了使用
<T: Request>這個(gè)泛型方式以外,我們還將host從Requestable移動(dòng)到了Client里,這是更適合它的地方
protocol ClientProtocol
{
// 基地址屬性
var host: String { get }
// 發(fā)送請(qǐng)求方法
func send<T: Requestable>(_ request: T, handler: @escaping (T.Response?) -> Void)
}
? 客戶端遵守客戶端協(xié)議
除了 URLSessionClient 以外,我們還可以使用任意的類型來(lái)滿足這個(gè)協(xié)議,并發(fā)送請(qǐng)求。這樣網(wǎng)絡(luò)層的具體實(shí)現(xiàn)和請(qǐng)求本身就不再相關(guān)了,我們之后在測(cè)試的時(shí)候會(huì)進(jìn)一步看到這么做所帶來(lái)的好處。
class URLSessionClient: ClientProtocol
{
// 創(chuàng)建客戶端管理者
static let manager = URLSessionClient()
// 給基地址賦值
let host: String = "http://127.0.0.1:5000"
// 實(shí)現(xiàn)發(fā)送請(qǐng)求方法
func send<T>(_ request: T, handler: @escaping (T.Response?) -> Void) where T : Requestable
{
...
}
}
? 實(shí)現(xiàn)發(fā)送請(qǐng)求方法
func send<T>(_ request: T, handler: @escaping (T.Response?) -> Void) where T : Requestable
{
// 請(qǐng)求地址 = 基地址 + request的傳入路徑
let url = URL(string: host.appending(request.path))!
// 根據(jù)url創(chuàng)建URLRequest
var urlRequest = URLRequest(url: url)
// 設(shè)置請(qǐng)求方法
urlRequest.httpMethod = request.method.rawValue
// 根據(jù)request創(chuàng)建dataTask并將請(qǐng)求發(fā)送
let task = URLSession.shared.dataTask(with: urlRequest)
// 使用 Response 中的 parse 方法將回調(diào)中的 data 轉(zhuǎn)換為合適的對(duì)象類型,并調(diào)用 handler 通知外部調(diào)用者
{ (data, response, error) in
// 調(diào)用Response里面的解碼方法將請(qǐng)求到的數(shù)據(jù)解碼成model后從主線程傳遞出去
if let data = data, let model = T.Response.parse(data: data)
{
DispatchQueue.main.async { handler(model) }
}
else
{
DispatchQueue.main.async { handler(nil) }
}
}
task.resume()
}
d、序列化能力者
? 解碼協(xié)議提供解碼方法
請(qǐng)求不應(yīng)該也不需要知道如何解析得到的數(shù)據(jù),這項(xiàng)工作應(yīng)該交給 Response 來(lái)做,而現(xiàn)在我們沒(méi)有對(duì) Response 進(jìn)行任何限定。接下來(lái)我們將新增一個(gè)協(xié)議,滿足這個(gè)協(xié)議的類型將知道如何將一個(gè) data 轉(zhuǎn)換為實(shí)際的數(shù)據(jù)類型。
對(duì)于 Person 我們知道可以使用 Person.init(data:)將json數(shù)據(jù)進(jìn)行轉(zhuǎn)化成數(shù)據(jù)模型,但是對(duì)于一般的 Response,我們還不知道要如何將數(shù)據(jù)轉(zhuǎn)為模型。DecodableProtocol要求滿足該協(xié)議的具體類型提供parse(data:) 方法合適的實(shí)現(xiàn),這樣提供轉(zhuǎn)換方法的任務(wù)就被“下放”到了各數(shù)據(jù)模型中。
protocol DecodableProtocol
{
static func parse(data: Data) -> Self?
}
DecodableProtocol 定義了一個(gè)靜態(tài)的 parse 方法,接下去我們需要在 Requestable 的 Response 關(guān)聯(lián)類型中為它加上這個(gè)限制,這樣我們可以保證所有的 Response 都可以對(duì)數(shù)據(jù)進(jìn)行解析。
protocol Requestable
{
var path: String { get }
var method: HTTPMethod { get }
var parameter: [String: Any] { get }
associatedtype Response: DecodableProtocol
}
? 遵守解碼協(xié)議實(shí)現(xiàn)解碼方法
extension Person: DecodableProtocol
{
static func parse(data: Data) -> Person?
{
// 傳入data獲取到Person,調(diào)用Person的初始化方法
return Person(data: data)
}
}
? 數(shù)據(jù)模型類Person
struct Person
{
// 屬性
let name: String
let age: String
let hobby: String
let petPhrase: String
// 初始化方法
init?(data: Data)
{
// [String: Any] 表示是字典類型
guard let person = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { return nil }
// 獲取person中的數(shù)據(jù)
guard let name = person["name"] as? String else { return nil }
guard let age = person["age"] as? String else { return nil }
guard let hobby = person["hobby"] as? String else { return nil }
guard let petPhrase = person["petPhrase"] as? String else { return nil }
// 給Person結(jié)構(gòu)體的屬性賦值
self.name = name
self.age = age
self.hobby = hobby
self.petPhrase = petPhrase
}
}
e、外界調(diào)用
當(dāng)然,你也可以為 URLSessionClient 添加一個(gè)單例來(lái)減少請(qǐng)求時(shí)的創(chuàng)建開(kāi)銷,或者為請(qǐng)求添加 Promise 的調(diào)用方式等等。在 POP 的組織下,這些改動(dòng)都很自然,也不會(huì)牽扯到請(qǐng)求的其他部分。你可以用和 UserRequest 類型相似的方式,為網(wǎng)絡(luò)層添加其他的 API 請(qǐng)求,只需要定義請(qǐng)求所必要的內(nèi)容,而不用擔(dān)心會(huì)觸及網(wǎng)絡(luò)方面的具體實(shí)現(xiàn)。
// 根據(jù)傳入的name創(chuàng)建request
let request = PersonRequest(name: "Xiejiapei")
// 客戶端發(fā)送request
URLSessionClient().send(request)
{ [weak self](person) in
// 根據(jù)服務(wù)端返回的數(shù)據(jù)更新UI
if let person = person
{
// 更新UI
print("\(person.hobby) from \(person.name)")
self?.updataUI(person: person)
}
}
f、進(jìn)行模塊劃分而不是全部堆砌在Request協(xié)議中的原因
倘若不進(jìn)行功能模塊劃分,而是將全部功能都放在Request協(xié)議中,就會(huì)變成下面這樣。這里最大的問(wèn)題在于,Request 協(xié)議管理了太多的東西。一個(gè) Request 協(xié)議應(yīng)該做的事情應(yīng)該僅僅是定義請(qǐng)求入口和期望的響應(yīng)類型,而現(xiàn)在 Request 協(xié)議不光定義了 host 的值,還對(duì)如何解析數(shù)據(jù)了如指掌。最后 send 方法被綁死在了 URLSession 的實(shí)現(xiàn)上,而且是作為 Request 協(xié)議的一部分存在,這是很不合理的,因?yàn)檫@意味著我們無(wú)法在不更改請(qǐng)求的情況下更新發(fā)送請(qǐng)求的方式,它們被耦合在了一起。這樣的結(jié)構(gòu)讓測(cè)試變得異常困難,我們可能需要通過(guò) stub 和 mock 的方式對(duì)請(qǐng)求攔截,然后返回構(gòu)造的數(shù)據(jù),這會(huì)用到 NSURLProtocol 的內(nèi)容,或者是引入一些第三方的測(cè)試框架,大大增加了項(xiàng)目的復(fù)雜度。
protocol Request
{
var host: String { get }
var path: String { get }
var method: HTTPMethod { get }
var parameter: [String: Any] { get }
associatedtype Response
func parse(data: Data) -> Response?
}
extension Request
{
func send(handler: @escaping (Response?) -> Void)
{
...
}
}
g、網(wǎng)絡(luò)層測(cè)試
將 Client 聲明為協(xié)議給我們帶來(lái)了額外的好處,那就是我們不再局限于使用某種特定的技術(shù) (比如這里的 URLSession) 來(lái)實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求。利用 POP,你只是定義了一個(gè)發(fā)送請(qǐng)求的協(xié)議,你可以很容易地使用像是 AFNetworking 或者 Alamofire 這樣的成熟的第三方框架來(lái)構(gòu)建具體的數(shù)據(jù)并處理請(qǐng)求的底層實(shí)現(xiàn)。我們甚至可以提供一組“虛假”的對(duì)請(qǐng)求的響應(yīng),用來(lái)進(jìn)行測(cè)試。這和傳統(tǒng)的 stub & mock 的方式在概念上是接近的,但是實(shí)現(xiàn)起來(lái)要簡(jiǎn)單得多,也明確得多。我們現(xiàn)在來(lái)看一看具體應(yīng)該怎么做。
我們先準(zhǔn)備一個(gè)文本文件,將它添加到項(xiàng)目的測(cè)試 target 中,作為網(wǎng)絡(luò)請(qǐng)求返回的內(nèi)容
// 文件名:usersXiejiapei
{"name":"姓名:謝佳培", "age": "年齡:23", "hobby": "愛(ài)好:讀書", "petPhrase": "格言:求知若渴,虛心若愚"}

接下來(lái),可以創(chuàng)建一個(gè)新的類型,讓它滿足 ClientProtocol 協(xié)議。但是與 URLSessionClient 不同,這個(gè)新類型的 send 方法并不會(huì)實(shí)際去創(chuàng)建請(qǐng)求,并發(fā)送給服務(wù)器。我們?cè)跍y(cè)試時(shí)需要驗(yàn)證的是一個(gè)請(qǐng)求發(fā)出后如果服務(wù)器正確響應(yīng),那么我們應(yīng)該也可以得到正確的模型實(shí)例。所以這個(gè)新的 LocalFileClient 需要做的事情就是從本地文件中加載定義好的結(jié)果,然后驗(yàn)證模型實(shí)例是否正確。
struct LocalFileClient: ClientProtocol
{
// 為了滿足 ClientProtocol 的要求,實(shí)際上我們不會(huì)發(fā)送請(qǐng)求
let host = ""
func send<T : Requestable>(_ request: T, handler: @escaping (T.Response?) -> Void)
{
switch request.path
{
case "/users/xiejiapei":
// 獲取fileURL
guard let fileURL = Bundle.main.url(forResource: "usersXiejiapei", withExtension: "") else { fatalError() }
// 根據(jù)fileURL獲取data
guard let data = try? Data(contentsOf: fileURL) else { fatalError() }
// 將data傳遞出去
handler(T.Response.parse(data: data))
default:
fatalError("Unknown path")
}
}
}
LocalFileClient 做的事情很簡(jiǎn)單,它先檢查輸入請(qǐng)求的 path 屬性,如果是 /users/Xiejiapei (也就是我們需要測(cè)試的請(qǐng)求),那么就從測(cè)試的 bundle 中讀取預(yù)先定義的文件,將其作為返回結(jié)果進(jìn)行 parse,然后調(diào)用 handler。如果我們需要增加其他請(qǐng)求的測(cè)試,可以添加新的 case 項(xiàng)。
在 LocalFileClient 的幫助下,現(xiàn)在可以很容易地對(duì) UserRequest 進(jìn)行測(cè)試了。通過(guò)這種方法,我們沒(méi)有依賴任何第三方測(cè)試庫(kù),也沒(méi)有使用 url 代理或者運(yùn)行時(shí)消息轉(zhuǎn)發(fā)等等這些復(fù)雜的技術(shù),就可以進(jìn)行請(qǐng)求測(cè)試了。保持簡(jiǎn)單的代碼和邏輯,對(duì)于項(xiàng)目維護(hù)和發(fā)展是至關(guān)重要的。
let client = LocalFileClient()
client.send(PersonRequest(name: "xiejiapei"))
{ [weak self](person) in
if let person = person
{
print("\(person.hobby) from \(person.name)")
self?.updataUI(person: person)
}
}

3、總結(jié)與解惑
a、總結(jié)
因?yàn)楦叨冉怦?,這種基于 POP 的實(shí)現(xiàn)為代碼的擴(kuò)展提供了相對(duì)寬松的可能性。我們剛才已經(jīng)說(shuō)過(guò),你不必自行去實(shí)現(xiàn)一個(gè)完整的 Client,而可以依賴于現(xiàn)有的網(wǎng)絡(luò)請(qǐng)求框架,實(shí)現(xiàn)請(qǐng)求發(fā)送的方法即可。也就是說(shuō),你也可以很容易地將某個(gè)正在使用的請(qǐng)求方式替換為另外的方式,而不會(huì)影響到請(qǐng)求的定義和使用。類似地,在 Response 的處理上,現(xiàn)在我們定義了 Decodable,用自己手寫的方式在解析模型。我們完全也可以使用任意的第三方 JSON 解析庫(kù),來(lái)幫助我們迅速構(gòu)建模型類型,這僅僅只需要實(shí)現(xiàn)一個(gè)將 Data 轉(zhuǎn)換為對(duì)應(yīng)模型類型的方法即可。
通過(guò)面向協(xié)議的編程,我們可以從傳統(tǒng)的繼承上解放出來(lái),用一種更靈活的方式,搭積木一樣對(duì)程序進(jìn)行組裝。每個(gè)協(xié)議專注于自己的功能,特別得益于協(xié)議擴(kuò)展,我們可以減少類和繼承帶來(lái)的共享狀態(tài)的風(fēng)險(xiǎn),讓代碼更加清晰。高度的協(xié)議化有助于解耦、測(cè)試以及擴(kuò)展,而結(jié)合泛型來(lái)使用協(xié)議,更可以讓我們免于動(dòng)態(tài)調(diào)用和類型轉(zhuǎn)換的苦惱,保證了代碼的安全性。
b、解惑
? 范例都是直接先寫 protocol,而不是 struct 或者 class,是不是我們?cè)趯?shí)踐 POP 的時(shí)候都應(yīng)該直接先定義協(xié)議?
我直接寫 protocol 是因?yàn)槲乙呀?jīng)對(duì)我要做什么有充分的了解。但是實(shí)際開(kāi)發(fā)的時(shí)候你可能會(huì)無(wú)法一開(kāi)始就寫出合適的協(xié)議定義。建議可以像我在 demo 中做的那樣,先“粗略”地進(jìn)行定義,然后通過(guò)不斷重構(gòu)來(lái)得到一個(gè)最終的版本。當(dāng)然,你也可以先用紙筆勾勒一個(gè)輪廓,然后再去定義和實(shí)現(xiàn)協(xié)議。當(dāng)然了,也沒(méi)人規(guī)定一定需要先定義協(xié)議,你完全也可以從普通類型開(kāi)始寫起,然后等發(fā)現(xiàn)共通點(diǎn)或者遇到我們之前提到的困境時(shí),再回頭看看是不是面向協(xié)議更加合適,這需要一定的 POP 經(jīng)驗(yàn)。
? 既然 POP 有這么多好處,那我們是不是不再需要面向?qū)ο?,可以全面轉(zhuǎn)向面向協(xié)議了?
答案可能讓你失望。在我們的日常項(xiàng)目中,每天打交道的 Cocoa 其實(shí)還是一個(gè)帶有濃厚 OOP 色彩的框架。也就是說(shuō),可能一段時(shí)期內(nèi)我們不可能拋棄 OOP。不過(guò) POP 其實(shí)可以和 OOP “和諧共處”,我們也已經(jīng)看到了不少使用 POP 改善代碼設(shè)計(jì)的例子。另外需要補(bǔ)充的是,POP 其實(shí)也并不是銀彈,它有不好的一面。最大的問(wèn)題是協(xié)議會(huì)增加代碼的抽象層級(jí) (這點(diǎn)上和類繼承是一樣的),特別是當(dāng)你的協(xié)議又繼承了其他協(xié)議的時(shí)候,這個(gè)問(wèn)題尤為嚴(yán)重。在經(jīng)過(guò)若干層的繼承后,滿足末端的協(xié)議會(huì)變得困難,你也難以確定某個(gè)方法究竟?jié)M足的是哪個(gè)協(xié)議的要求。這會(huì)讓代碼迅速變得復(fù)雜。如果一個(gè)協(xié)議并沒(méi)有能描述很多共通點(diǎn),或者說(shuō)能讓人很快理解的話,可能使用基本的類型還會(huì)更簡(jiǎn)單一些。
? 想問(wèn)一下你們?cè)陧?xiàng)目中使用 POP 的情況
我們?cè)陧?xiàng)目里用了很多 POP 的概念。上面 demo 里的網(wǎng)絡(luò)請(qǐng)求的例子就是從實(shí)際項(xiàng)目中抽出來(lái)的,我們覺(jué)得這樣的請(qǐng)求寫起來(lái)非常輕松,因?yàn)榇a很簡(jiǎn)單,新人進(jìn)來(lái)交接也十分愜意。除了模型層之外,我們?cè)?view 和 view controller 層也用了一些 POP 的代碼,比如支持分頁(yè)請(qǐng)求 tableview controller 的 NextPageLoadable,空列表時(shí)顯示頁(yè)面的 EmptyPage 等等。因?yàn)闀r(shí)間有限,不可能展開(kāi)一一說(shuō)明,所以這里我只挑選了一個(gè)具有代表性,又不是很復(fù)雜的網(wǎng)絡(luò)的例子。其實(shí)每個(gè)協(xié)議都讓我們的代碼,特別是 View Controller 變短,而且使測(cè)試變?yōu)榭赡?。可以說(shuō),我們的項(xiàng)目從 POP 受益良多,而且我們應(yīng)該會(huì)繼續(xù)使用下去。
二、初識(shí)Moya
1、Moya的簡(jiǎn)介
我們知道在 iOS 開(kāi)發(fā)中,可以使用 URLSession 進(jìn)行網(wǎng)絡(luò)請(qǐng)求。但為了方便起見(jiàn),我通常會(huì)選擇使用 Alamofire 這樣的第三方庫(kù)。這些庫(kù)本質(zhì)上也是基于 URLSession 的,但其封裝了許多細(xì)節(jié),可以讓我們網(wǎng)絡(luò)請(qǐng)求相關(guān)代碼(如獲取數(shù)據(jù),提交數(shù)據(jù),上傳文件,下載文件等)更加簡(jiǎn)潔易用。Moya 又是一個(gè)基于 Alamofire 的更高層網(wǎng)絡(luò)請(qǐng)求封裝抽象層。Moya 也就可以看做我們的網(wǎng)絡(luò)管理層,用來(lái)封裝 URL、參數(shù)等請(qǐng)求所需要的一些基本信息。使用后我們的客戶端代碼會(huì)直接操作 Moya,然后 Moya 去管理請(qǐng)求,而不用跟 Alamofire 進(jìn)行直接接觸。
在我們項(xiàng)目的 Service、View、或者 Model 文件中可能都會(huì)出現(xiàn)請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)的情況,如果直接使用 Alamofire,不僅很繁瑣,而且還會(huì)使代碼變得很混亂。過(guò)去我們通常的做法是在項(xiàng)目中添加一個(gè)網(wǎng)絡(luò)請(qǐng)求層ViewModel用來(lái)管理網(wǎng)絡(luò)請(qǐng)求,但這樣做可能會(huì)遇到一些問(wèn)題。
2、豆瓣范例

a、創(chuàng)建provider:如果要發(fā)起網(wǎng)絡(luò)請(qǐng)求就使用這個(gè) provider
// 初始化豆瓣FM請(qǐng)求的provider
let DouBanProvider = MoyaProvider<DouBan>()
b、請(qǐng)求類型
public enum DouBan
{
case channels //獲取頻道列表
case playlist(String) //獲取歌曲信息
}
c、配置請(qǐng)求信息
extension DouBan: TargetType
{
...
}
? 服務(wù)器地址
public var baseURL: URL
{
switch self
{
case .channels:
return URL(string: "https://www.douban.com")!
case .playlist(_):
return URL(string: "https://douban.fm")!
}
}
? 各個(gè)請(qǐng)求的具體路徑
public var path: String
{
switch self
{
case .channels:
return "/j/app/radio/channels"
case .playlist(_):
return "/j/mine/playlist"
}
}
? 請(qǐng)求方法類型
public var method: Moya.Method
{
return .get
}
? 請(qǐng)求任務(wù)事件(這里附帶上參數(shù))
public var task: Task
{
switch self
{
case .playlist(let channel):
var params: [String: Any] = [:]
params["channel"] = channel
params["type"] = "n"
params["from"] = "mainsite"
return .requestParameters(parameters: params, encoding: URLEncoding.default)
default:
return .requestPlain
}
}
? 是否執(zhí)行Alamofire驗(yàn)證
public var validate: Bool
{
return false
}
? 下面這個(gè)是做單元測(cè)試模擬的數(shù)據(jù),只會(huì)在單元測(cè)試文件中有作用
public var sampleData: Data
{
return "{}".data(using: String.Encoding.utf8)!
}
? 設(shè)置請(qǐng)求頭
public var headers: [String: String]?
{
return nil
}
d、使用我們的provider進(jìn)行網(wǎng)絡(luò)請(qǐng)求(獲取頻道列表數(shù)據(jù))
// 頻道列表數(shù)據(jù)
var channels:Array<JSON> = []
DouBanProvider.request(.channels)
{ result in
if case let .success(response) = result
{
// 解析數(shù)據(jù)
let data = try? response.mapJSON()
let json = JSON(data!)
self.channels = json["channels"].arrayValue
// 刷新表格數(shù)據(jù)
DispatchQueue.main.async
{
self.tableView.reloadData()
}
}
}
e、使用我們的provider進(jìn)行網(wǎng)絡(luò)請(qǐng)求(根據(jù)頻道ID獲取下面的歌曲)
DouBanProvider.request(.playlist(channelId))
{ result in
if case let .success(response) = result
{
// 解析數(shù)據(jù),獲取歌曲信息
let data = try? response.mapJSON()
let json = JSON(data!)
let music = json["song"].arrayValue[0]
let artist = music["artist"].stringValue
let title = music["title"].stringValue
let message = "歌手:\(artist)\n歌曲:\(title)"
// 將歌曲信息彈出顯示
self.showAlert(title: channelName, message: message)
}
}
3、訂單范例
a、準(zhǔn)備工作
- 請(qǐng)求地址:http://127.0.0.1:8080
- 公共請(qǐng)求頭:devtype:iOS,devid
- 公共請(qǐng)求參數(shù):token:"Gz1qYLXeBW8MZuUfDlr9wsAYuVS1cZFMJY9BbaF842L2gRps747o4w=="
| API | 參數(shù) | 說(shuō)明 |
|---|---|---|
| order/list | pageNO:訂單列表開(kāi)始頁(yè)碼,默認(rèn)從1開(kāi)始。 pageSize:每頁(yè)記錄數(shù) | 訂單列表 |
| order/findById | sn:訂單id | 根據(jù)id查詢訂單 |
b、配置請(qǐng)求信息
? 生成請(qǐng)求封裝類
let orderProvider = MoyaProvider<OrderApi>()
? 訂單相關(guān)api
enum OrderApi
{
case list(pageNO: Int = 1, pageSize: Int = 10) //很好的利用了枚舉綁定值這個(gè)特性
case findOne(sn: String)
}
c、實(shí)現(xiàn)TargetType協(xié)議
extension OrderApi: TargetType
{
...
}
? baseURL
var baseURL: URL
{
return URL(string: "http://127.0.0.1:8080/order")!
}
? 請(qǐng)求路徑
var path: String
{
switch self
{
case .list:
return "list"
case .findOne(_):
return "findById"
}
}
? 請(qǐng)求方式
var method: Moya.Method
{
return .post
}
? 解析格式
var sampleData: Data
{
return "{}".data(using: String.Encoding.utf8)!
}
? 創(chuàng)建請(qǐng)求任務(wù)
var task: Task
{
// 公共參數(shù)
var params: [String: Any] = ["token": "Gz1qYLXeBW8MZuUfDlr9wsAYuVS1cZFMJY9BbaF842L2gRps747o4w=="]
// 收集參數(shù)
switch self
{
case let .list(pageNO, pageSize):
params["pageNO"] = pageNO
params["pageSize"] = pageSize
case .findOne(let sn):
params["sn"] = sn
}
// 發(fā)起請(qǐng)求
return .requestParameters(parameters: params, encoding: URLEncoding.default)
}
? 公共請(qǐng)求頭
var headers: [String : String]?
{
return ["devtype": "iOS", "devid": UIDevice().identifierForVendor?.uuidString ?? "unknow"]
}
d、調(diào)用發(fā)送請(qǐng)求
orderProvider.request(OrderApi.findOne(sn: "DJKRE3248DFHJEW23"))
{ (result) in
print(result)
}
4、登錄范例
a、LoginAPI
類型
public enum LoginAPI
{
case login(String, String, String) // 登錄接口
case smscode(String) // 登錄,發(fā)送驗(yàn)證碼
case otherRequest // 其他接口,沒(méi)有參數(shù)
}
服務(wù)器地址
public var baseURL: URL
{
return URL(string:"http://127.0.0.1:5000/")!
}
各個(gè)請(qǐng)求的具體路徑
public var path: String
{
switch self
{
case .login:
return "login/"
case .smscode:
return "login/smscode/"
case .otherRequest:
return "login/otherRequest/"
}
}
請(qǐng)求方式
public var method: Moya.Method
{
switch self
{
case .login:
return .post
case .smscode:
return .post
default:
return .get
}
}
請(qǐng)求任務(wù)事件(這里附帶上參數(shù))
public var task: Task
{
var param:[String:Any] = [:]
switch self
{
case .login(let username,let password,let smscode):
param["username"] = username
param["password"] = password
param["smscode"] = smscode
case .smscode(let username):
param["username"] = username
default:
return .requestPlain
}
return .requestParameters(parameters: param, encoding: URLEncoding.default)
}
設(shè)置請(qǐng)求頭
public var headers: [String: String]?
{
return nil
}
b、LoginClient
static let manager = LoginClient()
發(fā)送驗(yàn)證碼
func smscode(username:String,complete:@escaping ((String) -> Void))
{
let provide = MoyaProvider<LoginAPI>()
provide.request(.smscode(username))
{ (result) in
switch result
{
case let .success(response):
let dict = LoginClient.myJson(data: response.data)
complete(dict["smscode"] as! String)
case let .failure(error):
print(error)
complete("")
}
}
}
進(jìn)行登錄
func login(username:String,password:String,smscode:String)
{
let provide = MoyaProvider<LoginAPI>()
provide.request(.login(username, password, smscode))
{ (result) in
switch result
{
case let .success(response):
let _ = LoginClient.myJson(data: response.data)
case let .failure(error):
print(error)
}
}
}
其他事件 - 比如注冊(cè)
func otherRequest()
{
let provide = MoyaProvider<LoginAPI>()
provide.request(.otherRequest)
{ (result) in
switch result
{
case let .success(response):
let _ = LoginClient.myJson(data: response.data)
case let .failure(error):
print(error)
}
}
}
序列化
static func myJson(data:Data?)->([String: Any])
{
guard let data = data else
{
print("data 為空")
return [:]
}
do
{
let dict = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
print("序列化字典: \(dict)")
return dict as! ([String : Any])
}
catch
{
print("序列化失敗")
return [:]
}
}
c、點(diǎn)擊登錄或者注冊(cè)
點(diǎn)擊發(fā)送驗(yàn)證碼
@objc func didClickCodeButton()
{
guard let username = usernameTF.text else
{
print("賬戶不可為空")
return
}
LoginClient.manager.smscode(username: username)
{ [weak self](smscode) in
self?.smscodeTF.text = smscode
}
}
點(diǎn)擊登錄
@objc func didClickLoginButton()
{
LoginClient.manager.login(username:usernameTF.text!, password: passwordTF.text!, smscode: smscodeTF.text!)
}
Demo
Demo在我的Github上,歡迎下載。
UseFrameworkDemo