面向協(xié)議編程與 Cocoa 的邂逅

文章轉(zhuǎn)載:https://onevcat.com/2016/11/pop-cocoa-1/

(作者非常棒,建議大家點(diǎn)進(jìn)去關(guān)注下作者)

本文是筆者在 MDCC 16 (移動(dòng)開發(fā)者大會(huì)) 上 iOS 專場中的主題演講的文字整理。您可以在這里找到演講使用的 Keynote,部分示例代碼可以在 MDCC 2016 的官方 repo中找到。因?yàn)槿績?nèi)容比較長,所以分成了上下兩個(gè)部分,本文 (上) 主要介紹了一些理論方面的內(nèi)容,包括面向?qū)ο缶幊檀嬖诘膯栴},面向協(xié)議的基本概念和決策模型等,下半部分主要展示了一些筆者日常使用面向協(xié)議思想和 Cocoa 開發(fā)結(jié)合的示例代碼,并對(duì)其進(jìn)行了一些解說。

引子

面向協(xié)議編程 (Protocol Oriented Programming,以下簡稱 POP) 是 Apple 在 2015 年 WWDC 上提出的 Swift 的一種編程范式。相比與傳統(tǒng)的面向?qū)ο缶幊?(OOP),POP 顯得更加靈活。結(jié)合 Swift 的值語義特性和 Swift 標(biāo)準(zhǔn)庫的實(shí)現(xiàn),這一年來大家發(fā)現(xiàn)了很多 POP 的應(yīng)用場景。本次演講希望能在介紹 POP 思想的基礎(chǔ)上,引入一些日常開發(fā)中可以使用 POP 的場景,讓與會(huì)來賓能夠開始在日常工作中嘗試 POP,并改善代碼設(shè)計(jì)。

起?初識(shí) - 什么是 Swift 協(xié)議

Protocol

Swift 標(biāo)準(zhǔn)庫中有 50 多個(gè)復(fù)雜不一的協(xié)議,幾乎所有的實(shí)際類型都是滿足若干協(xié)議的。protocol 是 Swift 語言的底座,語言的其他部分正是在這個(gè)底座上組織和建立起來的。這和我們熟知的面向?qū)ο蟮臉?gòu)建方式很不一樣。

一個(gè)最簡單但是有實(shí)際用處的 Swift 協(xié)議定義如下:

protocolGreetable{varname:String{get}funcgreet()}

這幾行代碼定義了一個(gè)名為Greetable的協(xié)議,其中有一個(gè)name屬性的定義,以及一個(gè)greet方法的定義。

所謂協(xié)議,就是一組屬性和/或方法的定義,而如果某個(gè)具體類型想要遵守一個(gè)協(xié)議,那它需要實(shí)現(xiàn)這個(gè)協(xié)議所定義的所有這些內(nèi)容。協(xié)議實(shí)際上做的事情不過是“關(guān)于實(shí)現(xiàn)的約定”。

面向?qū)ο?/p>

在深入 Swift 協(xié)議的概念之前,我想先重新讓大家回顧一下面向?qū)ο蟆O嘈盼覀儾徽撛诮炭茣蛘呤遣┛偷雀鞣N地方對(duì)這個(gè)名詞都十分熟悉了。那么有一個(gè)很有意思,但是其實(shí)并不是每個(gè)程序員都想過的問題,面向?qū)ο蟮暮诵乃枷刖烤故鞘裁矗?/p>

我們先來看一段面向?qū)ο蟮拇a:

classAnimal{varleg:Int{return2}funceat(){print("eat food.")}funcrun(){print("run with\(leg)legs")}}classTiger:Animal{overridevarleg:Int{return4}overridefunceat(){print("eat meat.")}}lettiger=Tiger()tiger.eat()// "eat meat"tiger.run()// "run with 4 legs"

父類Animal定義了動(dòng)物的leg(這里應(yīng)該使用虛類,但是 Swift 中沒有這個(gè)概念,所以先請(qǐng)無視這里的return 2),以及動(dòng)物的eat和run方法,并為它們提供了實(shí)現(xiàn)。子類的Tiger根據(jù)自身情況重寫了leg(4 條腿)和eat(吃肉),而對(duì)于run,父類的實(shí)現(xiàn)已經(jīng)滿足需求,因此不必重寫。

我們看到Tiger和Animal共享了一部分代碼,這部分代碼被封裝到了父類中,而除了Tiger的其他的子類也能夠使用Animal的這些代碼。這其實(shí)就是 OOP 的核心思想 - 使用封裝和繼承,將一系列相關(guān)的內(nèi)容放到一起。我們的前輩們?yōu)榱四軌驅(qū)φ鎸?shí)世界的對(duì)象進(jìn)行建模,發(fā)展出了面向?qū)ο缶幊痰母拍睿沁@套理念有一些缺陷。雖然我們努力用這套抽象和繼承的方法進(jìn)行建模,但是實(shí)際的事物往往是一系列特質(zhì)的組合,而不單單是以一脈相承并逐漸擴(kuò)展的方式構(gòu)建的。所以最近大家越來越發(fā)現(xiàn)面向?qū)ο蠛芏鄷r(shí)候其實(shí)不能很好地對(duì)事物進(jìn)行抽象,我們可能需要尋找另一種更好的方式。

面向?qū)ο缶幊痰睦Ь?/p>

橫切關(guān)注點(diǎn)

我們?cè)賮砜匆粋€(gè)例子。這次讓我們遠(yuǎn)離動(dòng)物世界,回到 Cocoa,假設(shè)我們有一個(gè)ViewController,它繼承自UIViewController,我們向其中添加一個(gè)myMethod:

classViewCotroller:UIViewController{// 繼承// view, isFirstResponder()...// 新加funcmyMethod(){}}

如果這時(shí)候我們又有一個(gè)繼承自UITableViewController的AnotherViewController,我們也想向其中添加同樣的myMethod:

classAnotherViewController:UITableViewController{// 繼承// tableView, isFirstResponder()...// 新加funcmyMethod(){}}

這時(shí),我們迎來了 OOP 的第一個(gè)大困境,那就是我們很難在不同繼承關(guān)系的類里共用代碼。這里的問題用“行話”來說叫做“橫切關(guān)注點(diǎn)” (Cross-Cutting Concerns)。我們的關(guān)注點(diǎn)myMethod位于兩條繼承鏈 (UIViewController->ViewCotroller和UIViewController->UITableViewController->AnotherViewController) 的橫切面上。面向?qū)ο笫且环N不錯(cuò)的抽象方式,但是肯定不是最好的方式。它無法描述兩個(gè)不同事物具有某個(gè)相同特性這一點(diǎn)。在這里,特性的組合要比繼承更貼切事物的本質(zhì)。

想要解決這個(gè)問題,我們有幾個(gè)方案:

Copy & Paste

這是一個(gè)比較糟糕的解決方案,但是演講現(xiàn)場還是有不少朋友選擇了這個(gè)方案,特別是在工期很緊,無暇優(yōu)化的情況下。這誠然可以理解,但是這也是壞代碼的開頭。我們應(yīng)該盡量避免這種做法。

引入 BaseViewController

在一個(gè)繼承自UIViewController的BaseViewController上添加需要共享的代碼,或者干脆在UIViewController上添加 extension。看起來這是一個(gè)稍微靠譜的做法,但是如果不斷這么做,會(huì)讓所謂的Base很快變成垃圾堆。職責(zé)不明確,任何東西都能扔進(jìn)Base,你完全不知道哪些類走了Base,而這個(gè)“超級(jí)類”對(duì)代碼的影響也會(huì)不可預(yù)估。

依賴注入

通過外界傳入一個(gè)帶有myMethod的對(duì)象,用新的類型來提供這個(gè)功能。這是一個(gè)稍好的方式,但是引入額外的依賴關(guān)系,可能也是我們不太愿意看到的。

多繼承

當(dāng)然,Swift 是不支持多繼承的。不過如果有多繼承的話,我們確實(shí)可以從多個(gè)父類進(jìn)行繼承,并將myMethod添加到合適的地方。有一些語言選擇了支持多繼承 (比如 C++),但是它會(huì)帶來 OOP 中另一個(gè)著名的問題:菱形缺陷。

菱形缺陷

上面的例子中,如果我們有多繼承,那么ViewController和AnotherViewController的關(guān)系可能會(huì)是這樣的:

在上面這種拓?fù)浣Y(jié)構(gòu)中,我們只需要在ViewController中實(shí)現(xiàn)myMethod,在AnotherViewController中也就可以繼承并使用它了。看起來很完美,我們避免了重復(fù)。但是多繼承有一個(gè)無法回避的問題,就是兩個(gè)父類都實(shí)現(xiàn)了同樣的方法時(shí),子類該怎么辦?我們很難確定應(yīng)該繼承哪一個(gè)父類的方法。因?yàn)槎嗬^承的拓?fù)浣Y(jié)構(gòu)是一個(gè)菱形,所以這個(gè)問題又被叫做菱形缺陷 (Diamond Problem)。像是 C++ 這樣的語言選擇粗暴地將菱形缺陷的問題交給程序員處理,這無疑非常復(fù)雜,并且增加了人為錯(cuò)誤的可能性。而絕大多數(shù)現(xiàn)代語言對(duì)多繼承這個(gè)特性選擇避而遠(yuǎn)之。

動(dòng)態(tài)派發(fā)安全性

Objective-C 恰如其名,是一門典型的 OOP 語言,同時(shí)它繼承了 Small Talk 的消息發(fā)送機(jī)制。這套機(jī)制十分靈活,是 OC 的基礎(chǔ)思想,但是有時(shí)候相對(duì)危險(xiǎn)。考慮下面的代碼:

ViewController*v1=...[v1myMethod];AnotherViewController*v2=...[v2myMethod];NSArray*array=@[v1,v2];for(idobjinarray){[objmyMethod];}

我們?nèi)绻赩iewController和AnotherViewController中都實(shí)現(xiàn)了myMethod的話,這段代碼是沒有問題的。myMethod將會(huì)被動(dòng)態(tài)發(fā)送給array中的v1和v2。但是,要是我們有一個(gè)沒有實(shí)現(xiàn)myMethod的類型,會(huì)如何呢?

NSObject*v3=[NSObjectnew]// v3 沒有實(shí)現(xiàn) `myMethod`NSArray*array=@[v1,v2,v3];for(idobjinarray){[objmyMethod];}// Runtime error:

// unrecognized selector sent to instance blabla

編譯依然可以通過,但是顯然,程序?qū)⒃谶\(yùn)行時(shí)崩潰。Objective-C 是不安全的,編譯器默認(rèn)你知道某個(gè)方法確實(shí)有實(shí)現(xiàn),這是消息發(fā)送的靈活性所必須付出的代價(jià)。而在 app 開發(fā)看來,用可能的崩潰來換取靈活性,顯然這個(gè)代價(jià)太大了。雖然這不是 OOP 范式的問題,但它確實(shí)在 Objective-C 時(shí)代給我們帶來了切膚之痛。

三大困境

我們可以總結(jié)一下 OOP 面臨的這幾個(gè)問題。

動(dòng)態(tài)派發(fā)安全性

橫切關(guān)注點(diǎn)

菱形缺陷

首先,在 OC 中動(dòng)態(tài)派發(fā)讓我們承擔(dān)了在運(yùn)行時(shí)才發(fā)現(xiàn)錯(cuò)誤的風(fēng)險(xiǎn),這很有可能是發(fā)生在上線產(chǎn)品中的錯(cuò)誤。其次,橫切關(guān)注點(diǎn)讓我們難以對(duì)對(duì)象進(jìn)行完美的建模,代碼的重用也會(huì)更加糟糕。

承?相知 - 協(xié)議擴(kuò)展和面向協(xié)議編程

使用協(xié)議解決 OOP 困境

協(xié)議并不是什么新東西,也不是 Swift 的發(fā)明。在 Java 和 C# 里,它叫做Interface。而 Swift 中的 protocol 將這個(gè)概念繼承了下來,并發(fā)揚(yáng)光大。讓我們回到一開始定義的那個(gè)簡單協(xié)議,并嘗試著實(shí)現(xiàn)這個(gè)協(xié)議:

protocolGreetable{varname:String{get}funcgreet()}

structPerson:Greetable{letname:Stringfuncgreet(){print("你好\(name)")}}Person(name:"Wei Wang").greet()

實(shí)現(xiàn)很簡單,Person結(jié)構(gòu)體通過實(shí)現(xiàn)name和greet來滿足Greetable。在調(diào)用時(shí),我們就可以使用Greetable中定義的方法了。

動(dòng)態(tài)派發(fā)安全性

除了Person,其他類型也可以實(shí)現(xiàn)Greetable,比如Cat:

structCat:Greetable{letname:Stringfuncgreet(){print("meow~\(name)")}}

現(xiàn)在,我們就可以將協(xié)議作為標(biāo)準(zhǔn)類型,來對(duì)方法調(diào)用進(jìn)行動(dòng)態(tài)派發(fā)了:

letarray:[Greetable]=[Person(name:"Wei Wang"),Cat(name:"onevcat")]forobjinarray{obj.greet()}// 你好 Wei Wang// meow~ onevcat

對(duì)于沒有實(shí)現(xiàn) Greetbale 的類型,編譯器將返回錯(cuò)誤,因此不存在消息誤發(fā)送的情況:

structBug:Greetable{letname:String}// Compiler Error:// 'Bug' does not conform to protocol 'Greetable'// protocol requires function 'greet()'

這樣一來,動(dòng)態(tài)派發(fā)安全性的問題迎刃而解。如果你保持在 Swift 的世界里,那這個(gè)你的所有代碼都是安全的。

? 動(dòng)態(tài)派發(fā)安全性

橫切關(guān)注點(diǎn)

菱形缺陷

橫切關(guān)注點(diǎn)

使用協(xié)議和協(xié)議擴(kuò)展,我們可以很好地共享代碼。回到上一節(jié)的myMethod方法,我們來看看如何使用協(xié)議來搞定它。首先,我們可以定義一個(gè)含有myMethod的協(xié)議:

protocolP{funcmyMethod()}

注意這個(gè)協(xié)議沒有提供任何的實(shí)現(xiàn)。我們依然需要在實(shí)際類型遵守這個(gè)協(xié)議的時(shí)候?yàn)樗峁┚唧w的實(shí)現(xiàn):

// class ViewController: UIViewControllerextensionViewController:P{funcmyMethod(){doWork()}}// class AnotherViewController: UITableViewControllerextensionAnotherViewController:P{funcmyMethod(){doWork()}}

你可能不禁要問,這和 Copy & Paste 的解決方式有何不同?沒錯(cuò),答案就是 – 沒有不同。不過稍安勿躁,我們還有其他科技可以解決這個(gè)問題,那就是協(xié)議擴(kuò)展。協(xié)議本身并不是很強(qiáng)大,只是靜態(tài)類型語言的編譯器保證,在很多靜態(tài)語言中也有類似的概念。那到底是什么讓 Swift 成為了一門協(xié)議優(yōu)先的語言?真正使協(xié)議發(fā)生質(zhì)變,并讓大家如此關(guān)注的原因,其實(shí)是在 WWDC 2015 和 Swift 2 發(fā)布時(shí),Apple 為協(xié)議引入了一個(gè)新特性,協(xié)議擴(kuò)展,它為 Swift 語言帶來了一次革命性的變化。

所謂協(xié)議擴(kuò)展,就是我們可以為一個(gè)協(xié)議提供默認(rèn)的實(shí)現(xiàn)。對(duì)于P,可以在extension P中為myMethod添加一個(gè)實(shí)現(xiàn):

protocolP{funcmyMethod()}extensionP{funcmyMethod(){doWork()}}

有了這個(gè)協(xié)議擴(kuò)展后,我們只需要簡單地聲明ViewController和AnotherViewController遵守P,就可以直接使用myMethod的實(shí)現(xiàn)了:

extensionViewController:P{}extensionAnotherViewController:P{}viewController.myMethod()anotherViewController.myMethod()

不僅如此,除了已經(jīng)定義過的方法,我們甚至可以在擴(kuò)展中添加協(xié)議里沒有定義過的方法。在這些額外的方法中,我們可以依賴協(xié)議定義過的方法進(jìn)行操作。我們之后會(huì)看到更多的例子。總結(jié)下來:

協(xié)議定義

提供實(shí)現(xiàn)的入口

遵循協(xié)議的類型需要對(duì)其進(jìn)行實(shí)現(xiàn)

協(xié)議擴(kuò)展

為入口提供默認(rèn)實(shí)現(xiàn)

根據(jù)入口提供額外實(shí)現(xiàn)

這樣一來,橫切點(diǎn)關(guān)注的問題也簡單安全地得到了解決。

? 動(dòng)態(tài)派發(fā)安全性

? 橫切關(guān)注點(diǎn)

菱形缺陷

菱形缺陷

最后我們看看多繼承。多繼承中存在的一個(gè)重要問題是菱形缺陷,也就是子類無法確定使用哪個(gè)父類的方法。在協(xié)議的對(duì)應(yīng)方面,這個(gè)問題雖然依然存在,但卻是可以唯一安全地確定的。我們來看一個(gè)多個(gè)協(xié)議中出現(xiàn)同名元素的例子:

protocolNameable{varname:String{get}}protocolIdentifiable{varname:String{get}varid:Int{get}}

如果有一個(gè)類型,需要同時(shí)實(shí)現(xiàn)兩個(gè)協(xié)議的話,它必須提供一個(gè)name屬性,來同時(shí)滿足兩個(gè)協(xié)議的要求:

structPerson:Nameable,Identifiable{letname:Stringletid:Int}// `name` 屬性同時(shí)滿足 Nameable 和 Identifiable 的 name

這里比較有意思,又有點(diǎn)讓人困惑的是,如果我們?yōu)槠渲械哪硞€(gè)協(xié)議進(jìn)行了擴(kuò)展,在其中提供了默認(rèn)的name實(shí)現(xiàn),會(huì)如何。考慮下面的代碼:

extensionNameable{varname:String{return"default name"}}structPerson:Nameable,Identifiable{// let name: Stringletid:Int}// Identifiable 也將使用 Nameable extension 中的 name

這樣的編譯是可以通過的,雖然Person中沒有定義name,但是通過Nameable的name(因?yàn)樗庆o態(tài)派發(fā)的),Person依然可以遵守Identifiable。不過,當(dāng)Nameable和Identifiable都有name的協(xié)議擴(kuò)展的話,就無法編譯了:

extensionNameable{varname:String{return"default name"}}extensionIdentifiable{varname:String{return"another default name"}}structPerson:Nameable,Identifiable{// let name: Stringletid:Int}// 無法編譯,name 屬性沖突

這種情況下,Person無法確定要使用哪個(gè)協(xié)議擴(kuò)展中name的定義。在同時(shí)實(shí)現(xiàn)兩個(gè)含有同名元素的協(xié)議,并且它們都提供了默認(rèn)擴(kuò)展時(shí),我們需要在具體的類型中明確地提供實(shí)現(xiàn)。這里我們將Person中的name進(jìn)行實(shí)現(xiàn)就可以了:

extensionNameable{varname:String{return"default name"}}extensionIdentifiable{varname:String{return"another default name"}}structPerson:Nameable,Identifiable{letname:Stringletid:Int}Person(name:"onevcat",id:123).name// onevcat

這里的行為看起來和菱形問題很像,但是有一些本質(zhì)不同。首先,這個(gè)問題出現(xiàn)的前提條件是同名元素以及同時(shí)提供了實(shí)現(xiàn),而協(xié)議擴(kuò)展對(duì)于協(xié)議本身來說并不是必須的。其次,我們?cè)诰唧w類型中提供的實(shí)現(xiàn)一定是安全和確定的。當(dāng)然,菱形缺陷沒有被完全解決,Swift 還不能很好地處理多個(gè)協(xié)議的沖突,這是 Swift 現(xiàn)在的不足。

? 動(dòng)態(tài)派發(fā)安全性

? 橫切關(guān)注點(diǎn)

?菱形缺陷

本文是筆者在 MDCC 16 (移動(dòng)開發(fā)者大會(huì)) 上 iOS 專場中的主題演講的文字整理。您可以在這里找到演講使用的 Keynote,部分示例代碼可以在 MDCC 2016 的官方 repo中找到。

上半部分主要介紹了一些理論方面的內(nèi)容,包括面向?qū)ο缶幊檀嬖诘膯栴},面向協(xié)議的基本概念和決策模型等。本文 (下) 主要展示了一些筆者日常使用面向協(xié)議思想和 Cocoa 開發(fā)結(jié)合的示例代碼,并對(duì)其進(jìn)行了一些解說。

轉(zhuǎn)?熱戀 - 在日常開發(fā)中使用協(xié)議

WWDC 2015 在 POP 方面有一個(gè)非常優(yōu)秀的主題演講:#408 Protocol-Oriented Programming in Swift。Apple 的工程師通過舉了畫圖表和排序兩個(gè)例子,來闡釋 POP 的思想。我們可以使用 POP 來解耦,通過組合的方式讓代碼有更好的重用性。不過在 #408 中,涉及的內(nèi)容偏向理論,而我們每天的 app 開發(fā)更多的面臨的還是和 Cocoa 框架打交道。在看過 #408 以后,我們就一直在思考,如何把 POP 的思想運(yùn)用到日常的開發(fā)中?

我們?cè)谶@個(gè)部分會(huì)舉一個(gè)實(shí)際的例子,來看看 POP 是如何幫助我們寫出更好的代碼的。

基于 Protocol 的網(wǎng)絡(luò)請(qǐng)求

網(wǎng)絡(luò)請(qǐng)求層是實(shí)踐 POP 的一個(gè)理想場所。我們?cè)诮酉碌睦又袑牧汩_始,用最簡單的面向協(xié)議的方式先構(gòu)建一個(gè)不那么完美的網(wǎng)絡(luò)請(qǐng)求和模型層,它可能包含一些不合理的設(shè)計(jì)和耦合,但是卻是初步最容易得到的結(jié)果。然后我們將逐步捋清各部分的所屬,并用分離職責(zé)的方式來進(jìn)行重構(gòu)。最后我們會(huì)為這個(gè)網(wǎng)絡(luò)請(qǐng)求層進(jìn)行測試。通過這個(gè)例子,我希望能夠設(shè)計(jì)出包括類型安全,解耦合,易于測試和良好的擴(kuò)展性等諸多優(yōu)秀特性在內(nèi)的 POP 代碼。

Talk is cheap, show me the code.

初步實(shí)現(xiàn)

首先,我們想要做的事情是從一個(gè) API 請(qǐng)求一個(gè) JSON,然后將它轉(zhuǎn)換為 Swift 中可用的實(shí)例。作為例子的 API 非常簡單,你可以直接訪問https://api.onevcat.com/users/onevcat來查看返回:

{"name":"onevcat","message":"Welcome to MDCC 16!"}

我們可以新建一個(gè)項(xiàng)目,并添加User.swift來作為模型:

// User.swiftimportFoundationstructUser{letname:Stringletmessage:Stringinit?(data:Data){guardletobj=try?JSONSerialization.jsonObject(with:data,options:[])as?[String:Any]else{returnnil}guardletname=obj?["name"]as?Stringelse{returnnil}guardletmessage=obj?["message"]as?Stringelse{returnnil}self.name=nameself.message=message}}

User.init(data:)將輸入的數(shù)據(jù) (從網(wǎng)絡(luò)請(qǐng)求 API 獲取) 解析為 JSON 對(duì)象,然后從中取出name和message,并構(gòu)建代表 API 返回的User實(shí)例,非常簡單。

現(xiàn)在讓我們來看看有趣的部分,也就是如何使用 POP 的方式從 URL 請(qǐng)求數(shù)據(jù),并生成對(duì)應(yīng)的User。首先,我們可以創(chuàng)建一個(gè) protocol 來代表請(qǐng)求。對(duì)于一個(gè)請(qǐng)求,我們需要知道它的請(qǐng)求路徑,HTTP 方法,所需要的參數(shù)等信息。一開始這個(gè)協(xié)議可能是這樣的:

enumHTTPMethod:String{caseGETcasePOST}protocolRequest{varhost:String{get}varpath:String{get}varmethod:HTTPMethod{get}varparameter:[String:Any]{get}}

將host和path拼接起來可以得到我們需要請(qǐng)求的 API 地址。為了簡化,HTTPMethod現(xiàn)在只包含了GET和POST兩種請(qǐng)求方式,而在我們的例子中,我們只會(huì)使用到GET請(qǐng)求。

現(xiàn)在,可以新建一個(gè)UserRequest來實(shí)現(xiàn)Request協(xié)議:

structUserRequest:Request{letname:Stringlethost="https://api.onevcat.com"varpath:String{return"/users/\(name)"}letmethod:HTTPMethod=.GETletparameter:[String:Any]=[:]}

UserRequest中有一個(gè)未定義初始值的name屬性,其他的屬性都是為了滿足協(xié)議所定義的。因?yàn)檎?qǐng)求的參數(shù)用戶名name會(huì)通過 URL 進(jìn)行傳遞,所以parameter是一個(gè)空字典就足夠了。有了協(xié)議定義和一個(gè)滿足定義的具體請(qǐng)求,現(xiàn)在我們需要發(fā)送請(qǐng)求。為了任意請(qǐng)求都可以通過同樣的方法發(fā)送,我們將發(fā)送的方法定義在Request協(xié)議擴(kuò)展上:

extensionRequest{funcsend(handler:@escaping(User?)->Void){// ... send 的實(shí)現(xiàn)}}

在send(handler:)的參數(shù)中,我們定義了可逃逸的(User?) -> Void,在請(qǐng)求完成后,我們調(diào)用這個(gè)handler方法來通知調(diào)用者請(qǐng)求是否完成,如果一切正常,則將一個(gè)User實(shí)例傳回,否則傳回nil。

我們想要這個(gè)send方法對(duì)于所有的Request都通用,所以顯然回調(diào)的參數(shù)類型不能是User。通過在Request協(xié)議中添加一個(gè)關(guān)聯(lián)類型,我們可以將回調(diào)參數(shù)進(jìn)行抽象。在Request最后添加:

protocolRequest{...associatedtypeResponse}

然后在UserRequest中,我們也相應(yīng)地添加類型定義,以滿足協(xié)議:

structUserRequest:Request{...typealiasResponse=User}

現(xiàn)在,我們來重新實(shí)現(xiàn)send方法,現(xiàn)在,我們可以用Response代替具體的User,讓send一般化。我們這里使用URLSession來發(fā)送請(qǐng)求:

extensionRequest{funcsend(handler:@escaping(Response?)->Void){leturl=URL(string:host.appending(path))!varrequest=URLRequest(url:url)request.httpMethod=method.rawValue// 在示例中我們不需要 `httpBody`,實(shí)踐中可能需要將 parameter 轉(zhuǎn)為 data// request.httpBody = ...lettask=URLSession.shared.dataTask(with:request){data,res,errorin// 處理結(jié)果print(data)}task.resume()}}

通過拼接host和path,可以得到 API 的 entry point。根據(jù)這個(gè) URL 創(chuàng)建請(qǐng)求,進(jìn)行配置,生成 data task 并將請(qǐng)求發(fā)送。剩下的工作就是將回調(diào)中的data轉(zhuǎn)換為合適的對(duì)象類型,并調(diào)用handler通知外部調(diào)用者了。對(duì)于User我們知道可以使用User.init(data:),但是對(duì)于一般的Response,我們還不知道要如何將數(shù)據(jù)轉(zhuǎn)為模型。我們可以在Request里再定義一個(gè)parse(data:)方法,來要求滿足該協(xié)議的具體類型提供合適的實(shí)現(xiàn)。這樣一來,提供轉(zhuǎn)換方法的任務(wù)就被“下放”到了UserRequest:

protocolRequest{...associatedtypeResponsefuncparse(data:Data)->Response?}structUserRequest:Request{...typealiasResponse=Userfuncparse(data:Data)->User?{returnUser(data:data)}}

有了將data轉(zhuǎn)換為Response的方法后,我們就可以對(duì)請(qǐng)求的結(jié)果進(jìn)行處理了:

extensionRequest{funcsend(handler:@escaping(Response?)->Void){leturl=URL(string:host.appending(path))!varrequest=URLRequest(url:url)request.httpMethod=method.rawValue// 在示例中我們不需要 `httpBody`,實(shí)踐中可能需要將 parameter 轉(zhuǎn)為 data// request.httpBody = ...lettask=URLSession.shared.dataTask(with:request){data,_,errorinifletdata=data,letres=parse(data:data){DispatchQueue.main.async{handler(res)}}else{DispatchQueue.main.async{handler(nil)}}}task.resume()}}

現(xiàn)在,我們來試試看請(qǐng)求一下這個(gè) API:

letrequest=UserRequest(name:"onevcat")request.send{userinifletuser=user{print("\(user.message)from\(user.name)")}}// Welcome to MDCC 16! from onevcat

重構(gòu),關(guān)注點(diǎn)分離

雖然能夠?qū)崿F(xiàn)需求,但是上面的實(shí)現(xiàn)可以說非常糟糕。讓我們看看現(xiàn)在Request的定義和擴(kuò)展:

protocolRequest{varhost:String{get}varpath:String{get}varmethod:HTTPMethod{get}varparameter:[String:Any]{get}associatedtypeResponsefuncparse(data:Data)->Response?}extensionRequest{funcsend(handler:@escaping(Response?)->Void){...}}

這里最大的問題在于,Request管理了太多的東西。一個(gè)Request應(yīng)該做的事情應(yīng)該僅僅是定義請(qǐng)求入口和期望的響應(yīng)類型,而現(xiàn)在Request不光定義了host的值,還對(duì)如何解析數(shù)據(jù)了如指掌。最后send方法被綁死在了URLSession的實(shí)現(xiàn)上,而且是作為Request的一部分存在。這是很不合理的,因?yàn)檫@意味著我們無法在不更改請(qǐng)求的情況下更新發(fā)送請(qǐng)求的方式,它們被耦合在了一起。這樣的結(jié)構(gòu)讓測試變得異常困難,我們可能需要通過 stub 和 mock 的方式對(duì)請(qǐng)求攔截,然后返回構(gòu)造的數(shù)據(jù),這會(huì)用到NSURLProtocol的內(nèi)容,或者是引入一些第三方的測試框架,大大增加了項(xiàng)目的復(fù)雜度。在 Objective-C 時(shí)期這可能是一個(gè)可選項(xiàng),但是在 Swift 的新時(shí)代,我們有好得多的方法來處理這件事情。

讓我們開始著手重構(gòu)剛才的代碼,并為它們加上測試吧。首先我們將send(handler:)從Request分離出來。我們需要一個(gè)單獨(dú)的類型來負(fù)責(zé)發(fā)送請(qǐng)求。這里基于 POP 的開發(fā)方式,我們從定義一個(gè)可以發(fā)送請(qǐng)求的協(xié)議開始:

protocolClient{funcsend(_r:Request,handler:@escaping(Request.Response?)->Void)}// 編譯錯(cuò)誤

從上面的聲明從語義上來說是挺明確的,但是因?yàn)镽equest是含有關(guān)聯(lián)類型的協(xié)議,所以它并不能作為獨(dú)立的類型來使用,我們只能夠?qū)⑺鳛轭愋图s束,來限制輸入?yún)?shù)request。正確的聲明方式應(yīng)當(dāng)是:

protocolClient{funcsend(_r:T,handler:@escaping(T.Response?)->Void)varhost:String{get}}

除了使用這個(gè)泛型方式以外,我們還將host從Request移動(dòng)到了Client里,這是更適合它的地方。現(xiàn)在,我們可以把含有send的Request協(xié)議擴(kuò)展刪除,重新創(chuàng)建一個(gè)類型來滿足Client了。和之前一樣,它將使用URLSession來發(fā)送請(qǐng)求:

structURLSessionClient:Client{lethost="https://api.onevcat.com"funcsend(_r:T,handler:@escaping(T.Response?)->Void){leturl=URL(string:host.appending(r.path))!varrequest=URLRequest(url:url)request.httpMethod=r.method.rawValuelettask=URLSession.shared.dataTask(with:request){data,_,errorinifletdata=data,letres=r.parse(data:data){DispatchQueue.main.async{handler(res)}}else{DispatchQueue.main.async{handler(nil)}}}task.resume()}}

現(xiàn)在發(fā)送請(qǐng)求的部分和請(qǐng)求本身分離開了,而且我們使用協(xié)議的方式定義了Client。除了URLSessionClient以外,我們還可以使用任意的類型來滿足這個(gè)協(xié)議,并發(fā)送請(qǐng)求。這樣網(wǎng)絡(luò)層的具體實(shí)現(xiàn)和請(qǐng)求本身就不再相關(guān)了,我們之后在測試的時(shí)候會(huì)進(jìn)一步看到這么做所帶來的好處。

現(xiàn)在這個(gè)的實(shí)現(xiàn)里還有一個(gè)問題,那就是Request的parse方法。請(qǐng)求不應(yīng)該也不需要知道如何解析得到的數(shù)據(jù),這項(xiàng)工作應(yīng)該交給Response來做。而現(xiàn)在我們沒有對(duì)Response進(jìn)行任何限定。接下來我們將新增一個(gè)協(xié)議,滿足這個(gè)協(xié)議的類型將知道如何將一個(gè)data轉(zhuǎn)換為實(shí)際的類型:

protocolDecodable{staticfuncparse(data:Data)->Self?}

Decodable定義了一個(gè)靜態(tài)的parse方法,現(xiàn)在我們需要在Request的Response關(guān)聯(lián)類型中為它加上這個(gè)限制,這樣我們可以保證所有的Response都可以對(duì)數(shù)據(jù)進(jìn)行解析,原來Request中的parse聲明也就可以移除了:

// 最終的 Request 協(xié)議protocolRequest{varpath:String{get}varmethod:HTTPMethod{get}varparameter:[String:Any]{get}// associatedtype Response// func parse(data: Data) -> Response?associatedtypeResponse:Decodable}

最后要做的就是讓User滿足Decodable,并且修改上面URLSessionClient的解析部分的代碼,讓它使用Response中的parse方法:

extensionUser:Decodable{staticfuncparse(data:Data)->User?{returnUser(data:data)}}structURLSessionClient:Client{funcsend(_r:T,handler:@escaping(T.Response?)->Void){...// if let data = data, let res = parse(data: data) {ifletdata=data,letres=T.Response.parse(data:data){...}}}

最后,將UserRequest中不再需要的host和parse等清理一下,一個(gè)類型安全,解耦合的面向協(xié)議的網(wǎng)絡(luò)層就呈現(xiàn)在我們眼前了。想要調(diào)用UserRequest時(shí),我們可以這樣寫:

URLSessionClient().send(UserRequest(name:"onevcat")){userinifletuser=user{print("\(user.message)from\(user.name)")}}

當(dāng)然,你也可以為URLSessionClient添加一個(gè)單例來減少請(qǐng)求時(shí)的創(chuàng)建開銷,或者為請(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)。

網(wǎng)絡(luò)層測試

將Client聲明為協(xié)議給我們帶來了額外的好處,那就是我們不在局限于使用某種特定的技術(shù) (比如這里的URLSession) 來實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求。利用 POP,你只是定義了一個(gè)發(fā)送請(qǐng)求的協(xié)議,你可以很容易地使用像是 AFNetworking 或者 Alamofire 這樣的成熟的第三方框架來構(gòu)建具體的數(shù)據(jù)并處理請(qǐng)求的底層實(shí)現(xiàn)。我們甚至可以提供一組“虛假”的對(duì)請(qǐng)求的響應(yīng),用來進(jìn)行測試。這和傳統(tǒng)的 stub & mock 的方式在概念上是接近的,但是實(shí)現(xiàn)起來要簡單得多,也明確得多。我們現(xiàn)在來看一看具體應(yīng)該怎么做。

我們先準(zhǔn)備一個(gè)文本文件,將它添加到項(xiàng)目的測試 target 中,作為網(wǎng)絡(luò)請(qǐng)求返回的內(nèi)容:

// 文件名:users:onevcat{"name":"Wei Wang","message":"hello"}

接下來,可以創(chuàng)建一個(gè)新的類型,讓它滿足Client協(xié)議。但是與URLSessionClient不同,這個(gè)新類型的send方法并不會(huì)實(shí)際去創(chuàng)建請(qǐng)求,并發(fā)送給服務(wù)器。我們?cè)跍y試時(shí)需要驗(yàn)證的是一個(gè)請(qǐng)求發(fā)出后如果服務(wù)器按照文檔正確響應(yīng),那么我們應(yīng)該也可以得到正確的模型實(shí)例。所以這個(gè)新的Client需要做的事情就是從本地文件中加載定義好的結(jié)果,然后驗(yàn)證模型實(shí)例是否正確:

structLocalFileClient:Client{funcsend(_r:T,handler:@escaping(T.Response?)->Void){switchr.path{case"/users/onevcat":guardletfileURL=Bundle(for:ProtocolNetworkTests.self).url(forResource:"users:onevcat",withExtension:"")else{fatalError()}guardletdata=try?Data(contentsOf:fileURL)else{fatalError()}handler(T.Response.parse(data:data))default:fatalError("Unknown path")}}// 為了滿足 `Client` 的要求,實(shí)際我們不會(huì)發(fā)送請(qǐng)求lethost=""}

LocalFileClient做的事情很簡單,它先檢查輸入請(qǐng)求的path屬性,如果是/users/onevcat(也就是我們需要測試的請(qǐng)求),那么就從測試的 bundle 中讀取預(yù)先定義的文件,將其作為返回結(jié)果進(jìn)行parse,然后調(diào)用handler。如果我們需要增加其他請(qǐng)求的測試,可以添加新的case項(xiàng)。另外,加載本地文件資源的部分應(yīng)該使用更通用的寫法,不過因?yàn)槲覀冞@里只是示例,就不過多糾結(jié)了。

在LocalFileClient的幫助下,現(xiàn)在可以很容易地對(duì)UserRequest進(jìn)行測試了:

functestUserRequest(){letclient=LocalFileClient()client.send(UserRequest(name:"onevcat")){userinXCTAssertNotNil(user)XCTAssertEqual(user!.name,"Wei Wang")}}

通過這種方法,我們沒有依賴任何第三方測試庫,也沒有使用 url 代理或者運(yùn)行時(shí)消息轉(zhuǎn)發(fā)等等這些復(fù)雜的技術(shù),就可以進(jìn)行請(qǐng)求測試了。保持簡單的代碼和邏輯,對(duì)于項(xiàng)目維護(hù)和發(fā)展是至關(guān)重要的。

可擴(kuò)展性

因?yàn)楦叨冉怦睿@種基于 POP 的實(shí)現(xiàn)為代碼的擴(kuò)展提供了相對(duì)寬松的可能性。我們剛才已經(jīng)說過,你不必自行去實(shí)現(xiàn)一個(gè)完整的Client,而可以依賴于現(xiàn)有的網(wǎng)絡(luò)請(qǐng)求框架,實(shí)現(xiàn)請(qǐng)求發(fā)送的方法即可。也就是說,你也可以很容易地將某個(gè)正在使用的請(qǐng)求方式替換為另外的方式,而不會(huì)影響到請(qǐng)求的定義和使用。類似地,在Response的處理上,現(xiàn)在我們定義了Decodable,用自己手寫的方式在解析模型。我們完全也可以使用任意的第三方 JSON 解析庫,來幫助我們迅速構(gòu)建模型類型,這僅僅只需要實(shí)現(xiàn)一個(gè)將Data轉(zhuǎn)換為對(duì)應(yīng)模型類型的方法即可。

如果你對(duì) POP 方式的網(wǎng)絡(luò)請(qǐng)求和模型解析感興趣的話,不妨可以看看APIKit這個(gè)框架,我們?cè)谑纠兴故镜姆椒ǎ沁@個(gè)框架的核心思想。

合?陪伴 - 使用協(xié)議幫助改善代碼設(shè)計(jì)

通過面向協(xié)議的編程,我們可以從傳統(tǒng)的繼承上解放出來,用一種更靈活的方式,搭積木一樣對(duì)程序進(jìn)行組裝。每個(gè)協(xié)議專注于自己的功能,特別得益于協(xié)議擴(kuò)展,我們可以減少類和繼承帶來的共享狀態(tài)的風(fēng)險(xiǎn),讓代碼更加清晰。

高度的協(xié)議化有助于解耦、測試以及擴(kuò)展,而結(jié)合泛型來使用協(xié)議,更可以讓我們免于動(dòng)態(tài)調(diào)用和類型轉(zhuǎn)換的苦惱,保證了代碼的安全性。

提問環(huán)節(jié)

主題演講后有幾位朋友提了一些很有意義的問題,在這里我也稍作整理。有可能問題和回答與當(dāng)時(shí)的情形會(huì)有小的出入,僅供參考。

我剛才在看 demo 的時(shí)候發(fā)現(xiàn),你都是直接先寫protocol,而不是struct或者class。是不是我們?cè)趯?shí)踐 POP 的時(shí)候都應(yīng)該直接先定義協(xié)議?

我直接寫protocol是因?yàn)槲乙呀?jīng)對(duì)我要做什么有充分的了解,并且希望演講不要超時(shí)。但是實(shí)際開發(fā)的時(shí)候你可能會(huì)無法一開始就寫出合適的協(xié)議定義。建議可以像我在 demo 中做的那樣,先“粗略”地進(jìn)行定義,然后通過不斷重構(gòu)來得到一個(gè)最終的版本。當(dāng)然,你也可以先用紙筆勾勒一個(gè)輪廓,然后再去定義和實(shí)現(xiàn)協(xié)議。當(dāng)然了,也沒人規(guī)定一定需要先定義協(xié)議,你完全也可以從普通類型開始寫起,然后等發(fā)現(xiàn)共通點(diǎn)或者遇到我們之前提到的困境時(shí),再回頭看看是不是面向協(xié)議更加合適,這需要一定的 POP 經(jīng)驗(yàn)。

既然 POP 有這么多好處,那我們是不是不再需要面向?qū)ο螅梢匀孓D(zhuǎn)向面向協(xié)議了?

答案可能讓你失望。在我們的日常項(xiàng)目中,每天打交道的 Cocoa 其實(shí)還是一個(gè)帶有濃厚 OOP 色彩的框架。也就是說,可能一段時(shí)期內(nèi)我們不可能拋棄 OOP。不過 POP 其實(shí)可以和 OOP “和諧共處”,我們也已經(jīng)看到了不少使用 POP 改善代碼設(shè)計(jì)的例子。另外需要補(bǔ)充的是,POP 其實(shí)也并不是銀彈,它有不好的一面。最大的問題是協(xié)議會(huì)增加代碼的抽象層級(jí) (這點(diǎn)上和類繼承是一樣的),特別是當(dāng)你的協(xié)議又繼承了其他協(xié)議的時(shí)候,這個(gè)問題尤為嚴(yán)重。在經(jīng)過若干層的繼承后,滿足末端的協(xié)議會(huì)變得困難,你也難以確定某個(gè)方法究竟?jié)M足的是哪個(gè)協(xié)議的要求。這會(huì)讓代碼迅速變得復(fù)雜。如果一個(gè)協(xié)議并沒有能描述很多共通點(diǎn),或者說能讓人很快理解的話,可能使用基本的類型還會(huì)更簡單一些。

謝謝你的演講,想問一下你們?cè)陧?xiàng)目中使用 POP 的情況

我們?cè)陧?xiàng)目里用了很多 POP 的概念。上面 demo 里的網(wǎng)絡(luò)請(qǐng)求的例子就是從實(shí)際項(xiàng)目中抽出來的,我們覺得這樣的請(qǐng)求寫起來非常輕松,因?yàn)榇a很簡單,新人進(jìn)來交接也十分愜意。除了模型層之外,我們?cè)?view 和 view controller 層也用了一些 POP 的代碼,比如從 nib 創(chuàng)建 view 的NibCreatable,支持分頁請(qǐng)求 tableview controller 的NextPageLoadable,空列表時(shí)顯示頁面的EmptyPage等等。因?yàn)闀r(shí)間有限,不可能展開一一說明,所以這里我只挑選了一個(gè)具有代表性,又不是很復(fù)雜的網(wǎng)絡(luò)的例子。其實(shí)每個(gè)協(xié)議都讓我們的代碼,特別是 View Controller 變短,而且使測試變?yōu)榭赡堋?梢哉f,我們的項(xiàng)目從 POP 受益良多,而且我們應(yīng)該會(huì)繼續(xù)使用下去。

推薦資料

幾個(gè)我認(rèn)為在 POP 實(shí)踐中值得一看的資料,愿意再進(jìn)行深入了解的朋友不妨一看。

Protocol-Oriented Programming in Swift- WWDC 15 #408

Protocols with Associated Types- @alexisgallagher

Protocol Oriented Programming in the Real World- @_matthewpalmer

Practical Protocol-Oriented-Programming- @natashatherobot

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,273評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,527評(píng)論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,010評(píng)論 1 322
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,250評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,769評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,656評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,853評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,103評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評(píng)論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,487評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,815評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容