【轉(zhuǎn)】如何提高代碼的可讀性? -讀《編寫可讀代碼的藝術(shù)》

一. 為什么讀這本書

很多同行在編寫代碼的時(shí)候往往只關(guān)注一些宏觀上的主題:架構(gòu),設(shè)計(jì)模式癌幕,數(shù)據(jù)結(jié)構(gòu)等等噩咪,卻忽視了一些更細(xì)節(jié)上的點(diǎn):比如變量如何命名與使用羽戒,控制流的設(shè)計(jì),以及注釋的寫法等等塘偎。以上這些細(xì)節(jié)上的東西可以用代碼的可讀性來概括疗涉。

不同于宏觀上的架構(gòu)拿霉,設(shè)計(jì)模式等需要好幾個(gè)類,好幾個(gè)模塊才能看出來:代碼的可讀性是能夠立刻從微觀上的咱扣,一個(gè)變量的命名绽淘,函數(shù)的邏輯劃分,注釋的信息質(zhì)量里面看出來的闹伪。

宏觀層面上的東西固然重要沪铭,但是代碼的可讀性也屬于評(píng)價(jià)代碼質(zhì)量的一個(gè)無(wú)法讓人忽視的指標(biāo):它影響了閱讀代碼的成本(畢竟代碼注意是給人看的),甚至?xí)绊懘a出錯(cuò)的概率偏瓤!

這里引用《編寫可讀代碼的藝術(shù)》這本書里的一句話:

對(duì)于一個(gè)整體的軟件系統(tǒng)而言杀怠,既需要宏觀的架構(gòu)決策,設(shè)計(jì)與指導(dǎo)原則厅克,也必須重視圍觀的代碼細(xì)節(jié)赔退。在軟件歷史中,有許多影響深遠(yuǎn)的重大失敗证舟,其根源往往是編碼細(xì)節(jié)出現(xiàn)了疏漏硕旗。

因此筆者認(rèn)為代碼的可讀性可以作為考量一名程序員專業(yè)程度的指標(biāo)。

或許已經(jīng)有很多同行也正在努力提高自己代碼的可讀性女责。然而這里有一個(gè)很典型的錯(cuò)覺(筆者之前就有這種錯(cuò)覺)是:越少的代碼越容易讓人理解漆枚。

但是事實(shí)上,并不是代碼越精簡(jiǎn)就越容易讓人理解鲤竹。相對(duì)于追求最小化代碼行數(shù)浪读,一個(gè)更好的提高可讀性方法是最小化人們理解代碼所需要的時(shí)間。

這就引出了這本中的一個(gè)核心定理:

可讀性基本定理:代碼的寫法應(yīng)當(dāng)使別人理解它所需要的時(shí)間最小化辛藻。

正式這句話深深地吸引了我碘橘,于是決定利用這兩周的業(yè)余時(shí)間讀完并總結(jié)了這本書。

這本書講的就是關(guān)于”如何提高代碼的可讀性“吱肌。

總結(jié)下來痘拆,這本書從淺入深,在三個(gè)層次告訴了我們?nèi)绾巫尨a易于理解:

表層上的改進(jìn):在命名方法(變量名氮墨,方法名)纺蛆,變量聲明,代碼格式规揪,注釋等方面的改進(jìn)桥氏。

控制流和邏輯的改進(jìn):在控制流,邏輯表達(dá)式上讓代碼變得更容易理解猛铅。

結(jié)構(gòu)上的改進(jìn):善于抽取邏輯字支,借助自然語(yǔ)言的描述來改善代碼。

二. 表層的改進(jìn)

首先來講最近簡(jiǎn)單的一層如何改進(jìn),涉及到以下幾點(diǎn):

如何命名

如何聲明與使用變量

如何簡(jiǎn)化表達(dá)式

如何讓代碼具有美感

如何寫注釋

如何命名

關(guān)于如何命名堕伪,作者提出了一個(gè)關(guān)鍵思想:

關(guān)鍵思想:把盡可能多的信息裝入名字中揖庄。

這里的多指的是有價(jià)值的多。那么如何做到有價(jià)值呢欠雌?作者介紹了以下幾個(gè)建議:

選擇專業(yè)的詞匯蹄梢,避免泛泛的名字

給名字附帶更多信息

決定名字最適合的長(zhǎng)度

名字不能引起歧義

選擇專業(yè)的詞匯,避免泛泛的名字

一個(gè)比較常見的反例:get?富俄。

get這個(gè)詞最好是用來做輕量級(jí)的取方法的開頭禁炒,而如果用到其他的地方就會(huì)顯得很不專業(yè)。

舉個(gè)書中的例子:

getPage(url)

通過這個(gè)方法名很難判斷出這個(gè)方法是從緩存中獲取頁(yè)面數(shù)據(jù)還是從網(wǎng)頁(yè)中獲取蛙酪。如果是從網(wǎng)頁(yè)中獲取齐苛,更專業(yè)的詞應(yīng)該是fetchPage(url)或者downloadPage(url)翘盖。

還有一個(gè)比較常見的反例:returnValue和retval桂塞。這兩者都是“返回值”的意思,他們被濫用在各個(gè)有返回值的函數(shù)里面馍驯。其實(shí)這兩個(gè)次除了攜帶他們本來的意思返回值以外并不具備任何其他的信息阁危,是典型的泛泛的名字。

那么如何選擇一個(gè)專業(yè)的詞匯呢汰瘫?答案是在非常貼近你自己的意圖的基礎(chǔ)上狂打,選擇一個(gè)富有表現(xiàn)力的詞匯。

舉幾個(gè)例子:

相對(duì)于make混弥,選擇create,generate,build等詞匯會(huì)更有表現(xiàn)力趴乡,更加專業(yè)。

相對(duì)于find蝗拿,選擇search,extract,recover等詞匯會(huì)更有表現(xiàn)力晾捏,更加專業(yè)。

相對(duì)于retval哀托,選擇一個(gè)能充分描述這個(gè)返回值的性質(zhì)的名字惦辛,例如:

vareuclidean_norm =function(v){

varretval =0.0;

for(vari =0; i < v.length; i +=1;)

? ? ? retval += v[i] * v[i];

returnMatch.sqrt(retval);

}

這里的retval表示的是“平方的和”,因此sum_squares這個(gè)詞更加貼切你的意圖仓手,更加專業(yè)胖齐。

但是,有些情況下嗽冒,泛泛的名字也是有意義的呀伙,例如一個(gè)交換變量的情景:

if(right < left){

? ? tmp = right;

? ? right = left;

? ? left = temp;

}

像上面這種tmp只是作為一個(gè)臨時(shí)存儲(chǔ)的情況下,tmp表達(dá)的意思就比較貼切了添坊。因此剿另,像tmp這個(gè)名字,只適用于短期存在而且特性為臨時(shí)性的變量。

給名字附帶更多信息

除了選擇一個(gè)專業(yè)驰弄,貼切意圖的詞匯麻汰,我們也可以通過添加一些前后綴來給這個(gè)詞附帶更多的信息。這里所指的更多的信息有三種:

變量的單位

變量的屬性

變量的格式

為變量添加單位

有些變量是有單位的戚篙,在變量名的后面添加其單位可以讓這個(gè)變量名攜帶更多信息:

一個(gè)表達(dá)時(shí)間間隔的變量五鲫,它的單位是秒:相對(duì)于duraction,ducation_secs攜帶了更多的信息

一個(gè)表達(dá)內(nèi)存大小的變量岔擂,它的單位是mb:相對(duì)于size位喂,cache_mb攜帶了更多的信息。

為變量添加重要屬性

有些變量是具有一些非常重要的屬性乱灵,其重要程度是不允許使用者忽略的塑崖。例如:

一個(gè)UTF-8格式的html字節(jié),相對(duì)于html痛倚,html_utf8更加清楚地描述了這個(gè)變量的格式规婆。

一個(gè)純文本,需要加密的密碼字符串:相對(duì)于password蝉稳,plaintext_password更清楚地描述了這個(gè)變量的特點(diǎn)抒蚜。

為變量選擇適當(dāng)?shù)母袷?/a>

對(duì)于命名,有些既定的格式需要注意:

使用大駝峰命名來表示類名:HomeViewController耘戚。

使用小駝峰命名來表示屬性名:userNameLabel嗡髓。

使用下劃線連接詞來表示變量名:product_id。

使用kConstantName來表示常量:kCacheDuraction收津。

使用MACRO_NAME來表示宏:SCREEN_WIDTH饿这。

決定名字最適合的長(zhǎng)度

名字越長(zhǎng)越難記住,名字越短所持有的信息就越少撞秋,如何決定名字的長(zhǎng)度呢长捧?這里有幾個(gè)原則:

如果變量的作用域很小,可以取很短的名字

駝峰命名中的單元不能超過3個(gè)

不能使用大家不熟悉的縮寫

丟掉不必要的單元

如果變量的作用域很小部服,可以取很短的名字

如果一個(gè)變量作用域很兴艚恪:則可以給它取一個(gè)很短的名字也無(wú)妨。

看下面這個(gè)例子:

if(debug){

map m;

? ? LookUpNamesNumbers(&m);

? ? Print(m);

}

在這里廓八,變量的類型和使用范圍一眼可見奉芦,讀者可以了解這段代碼的所有信息,所以即使是取m這個(gè)非常簡(jiǎn)短的名字剧蹂,也不影響讀者來理解作者的意圖声功。

相反的,如果m是一個(gè)全局變量宠叼,當(dāng)你看到下面這段代碼就會(huì)很頭疼先巴,因?yàn)槟悴恢浪念愋筒⒉幻鞔_:

LookUpNamesNumbers(&m);

Print(m);

駝峰命名中的單元不能超過3個(gè)

我們知道駝峰命名可以很清晰地體現(xiàn)變量的含義其爵,但是當(dāng)駝峰命名中的單元超過了3個(gè)之后,就會(huì)很影響閱讀體驗(yàn):

userFriendsInfoModel

memoryCacheCalculateTool

是不是看上去很吃力伸蚯?因?yàn)槲覀兇竽X同時(shí)可以記住的信息非常有限摩渺,尤其是在看代碼的時(shí)候,這種短期記憶的局限性是無(wú)法讓我們同時(shí)記住或者瞬間理解幾個(gè)具有3~4個(gè)單元的變量名的剂邮。所以我們需要在變量名里面去除一些不必要的單元:

丟掉不必要的單元

有些單元在變量里面是可以去掉的摇幻,例如:

convertToString可以省略成toString。

不能使用大家不熟悉的縮寫

有些縮寫是大家熟知的:

doc?可以代替document

str?可以代替string

但是如果你想用BEManager來代替BackEndManager就比較不合適了挥萌。因?yàn)椴涣私獾娜藥缀跏菬o(wú)法猜到這個(gè)名稱的意義的绰姻。

所以類似這種情況不能偷懶,該是什么就是什么引瀑,否則會(huì)起到相反的效果狂芋。因?yàn)樗雌饋矸浅D吧覀兪熘囊恍┛s寫規(guī)則相去甚遠(yuǎn)憨栽。

名字不能引起歧義

有些名字會(huì)引起歧義帜矾,例如:

filter:過濾這個(gè)詞,可以是過濾出符合標(biāo)準(zhǔn)的徒像,也可以是減少不符合標(biāo)準(zhǔn)的:是兩種完全相反的結(jié)果黍特,所以不推薦使用。

clip:類似的锯蛀,到底是在原來的基礎(chǔ)上截掉某一段還是另外截出來某一段呢?同樣也不推薦使用次慢。

布爾值:read_password:是表達(dá)需要讀取密碼旁涤,還是已經(jīng)讀了密碼呢?所以最好使用need_password或者is_authenticated來代替比較好迫像。通常來說劈愚,給布爾值的變量加上is,has,can,should這樣的詞可以使布爾值表達(dá)的意思更加明確

這一節(jié)講了很多關(guān)于如何起好一個(gè)變量名的方法。其實(shí)有一個(gè)很簡(jiǎn)單的原則來判斷這個(gè)變量名起的是否是好的:那就是:團(tuán)隊(duì)的新成員是否能迅速理解這個(gè)變量名的含義闻妓。如果是菌羽,那么這個(gè)命名就是成功的,否則就不要偷懶了由缆,起個(gè)好名字注祖,對(duì)誰(shuí)都好。其實(shí)如果你養(yǎng)成習(xí)慣多花幾秒鐘想出個(gè)好名字均唉,你會(huì)發(fā)現(xiàn)你的“命名能力”會(huì)很快提升是晨。

如何聲明與使用變量

在寫程序的過程中我們會(huì)聲明很多變量(成員變量,臨時(shí)變量)舔箭,而我們要知道變量的聲明與使用策略是會(huì)對(duì)代碼的可讀性造成影響的:

變量越多罩缴,越難跟蹤它們的動(dòng)向。

變量的作用域越大,就需要跟蹤它們的動(dòng)向越久箫章。

變量改變的越頻繁烙荷,就越難跟蹤它的當(dāng)前值。

相對(duì)的檬寂,對(duì)于變量的聲明與使用奢讨,我們可以從這四個(gè)角度來提高代碼的可讀性:

減少變量的個(gè)數(shù)

縮小變量的作用域

縮短變量聲明與使用其代碼的距離

變量最好只寫一次

減少變量的個(gè)數(shù)

在一個(gè)函數(shù)里面可能會(huì)聲明很多變量,但是有些變量的聲明是毫無(wú)意義的焰薄,比如:

沒有價(jià)值的臨時(shí)變量

表示中間結(jié)果的變量

沒有價(jià)值的臨時(shí)變量

有些變量的聲明完全是多此一舉拿诸,它們的存在反而加大了閱讀代碼的成本:

letnow = datetime.datatime.now()

root_message.last_view_time = now

上面這個(gè)now變量的存在是毫無(wú)意義的,因?yàn)椋?/p>

沒有拆分任何復(fù)雜的表達(dá)式

datetime.datatime.now已經(jīng)很清楚地表達(dá)了意思

只使用了一次塞茅,因此而沒有壓縮任何冗余的代碼

所以完全不用這個(gè)變量也是完全可以的:

1root_message.last_view_time = datetime.datatime.now()

表示中間結(jié)果的變量

有的時(shí)候?yàn)榱诉_(dá)成一個(gè)目標(biāo)亩码,把一件事情分成了兩件事情來做,這兩件事情中間需要一個(gè)變量來傳遞結(jié)果野瘦。但往往這件事情不需要分成兩件事情來做描沟,這個(gè)“中間結(jié)果”也就不需要了:

看一個(gè)比較常見的需求,一個(gè)把數(shù)組中的某個(gè)值移除的例子:

//成員變量鞭光,比較難追蹤

classLargeCass{

? string str_;


voidMethod1(){

? ? str_ = ...;

? ? Method2();

? }


voidMethod2(){

//using str_

? }

}

降格:

//局部變量吏廉,容易追蹤

classLargeCass{


voidMethod1(){

? ? string str = ...;

? ? Method2(str);

? }


voidMethod2(string str){

//using str

? }

}

所以在設(shè)計(jì)類的時(shí)候如果這個(gè)數(shù)據(jù)(變量)可以通過方法參數(shù)來傳遞,就不要以成員變量來保存它惰许。

縮短變量聲明與使用其代碼的距離

在實(shí)現(xiàn)一個(gè)函數(shù)的時(shí)候席覆,我們可能會(huì)聲明比較多的變量,但這些變量的使用位置卻不都是在函數(shù)開頭汹买。

有一個(gè)比較不好的習(xí)慣就是無(wú)論變量在當(dāng)前函數(shù)的哪個(gè)位置使用佩伤,都在一開始(函數(shù)的開頭)就聲明了它們。這樣可能導(dǎo)致的問題是:閱讀代碼的人讀到函數(shù)后半部分的時(shí)候就忘記了這個(gè)變量的類型和初始值晦毙;而且因?yàn)樵诤瘮?shù)的開頭就聲明了好幾個(gè)變量生巡,也對(duì)閱讀代碼的人的大腦造成了負(fù)擔(dān),因?yàn)槿说亩唐谟洃浭怯邢薜募剩貏e是記一些暫時(shí)還不知道怎么用的東西孤荣。

因此,如果在函數(shù)內(nèi)部需要在不同地方使用幾個(gè)不同的變量须揣,建議在真正使用它們之前再聲明它盐股。

變量最好只寫一次

操作一個(gè)變量的地方越多,就越難確定它的當(dāng)前值返敬。所以在很多語(yǔ)言里面有其各自的方式讓一些變量不可變(是個(gè)常量)遂庄,比如C++里的const和Java中的final。

如何簡(jiǎn)化表達(dá)式

有些表達(dá)式比較長(zhǎng)劲赠,很難讓人馬上理解涛目。這時(shí)候最好可以將其拆分成更容易的幾個(gè)小塊秸谢。可以嘗試下面的幾個(gè)方法:

使用解釋變量

使用總結(jié)變量

使用德摩根定理

使用解釋變量

有些變量會(huì)從一個(gè)比較長(zhǎng)的算式得出霹肝,這個(gè)表達(dá)式可能很難讓人看懂估蹄。這時(shí)候就需要用一個(gè)簡(jiǎn)短的“解釋”變量來詮釋算式的含義。使用書中的一個(gè)例子:

1ifline.split(':')[0].strip() =="root"

其實(shí)上面左側(cè)的表達(dá)式其實(shí)得出的是用戶名沫换,我們可以用username來替換它:

username = line.split(':')[0].strip()

ifusername =="root"

使用總結(jié)變量

除了以“變量”替換“算式”臭蚁,還可以用“變量”來替換含有更多變量更復(fù)雜的內(nèi)容,比如條件語(yǔ)句讯赏,這時(shí)候該變量可以被稱為”總結(jié)變量”垮兑。使用書中的一個(gè)例子:

if(request.user.id == document.owner_id){

//do something

}

上面這條判斷語(yǔ)句所判斷的是:“該文檔的所有者是不是該用戶”。我們可以使用一個(gè)總結(jié)性的變量user_owns_document來替換它:

finalbooleanuser_owns_document = (request.user.id == document.owner_id);

if(user_owns_document){

//do something

}

使用德摩根定理

德摩根定理:

not(a or b or c)等價(jià)于(not a) and (not b) and (not c)

not(a and b and c)等價(jià)于(not a) or (not b) or (not c)

當(dāng)我們條件語(yǔ)句里面存在外部取反的情況漱挎,就可以使用德摩根定理來做個(gè)轉(zhuǎn)換系枪。使用書中的一個(gè)例子:

//使用德摩根定理轉(zhuǎn)換以前

if(!(file_exists && !is_protected)){}

//使用德摩根定理轉(zhuǎn)換以后

if(!file_exists || is_protected){}

如何讓代碼具有美感

在讀過一些好的源碼之后我有一個(gè)感受:好的源碼往往都看上去都很漂亮,很有美感磕谅。這里說的漂亮和美感不是指代碼的邏輯清晰有條理私爷,而是指感官上的視覺感受讓人感覺很舒服。這是從一種純粹的審美的角度來評(píng)價(jià)代碼的:富有美感的代碼讓人賞心悅目膊夹,也容易讓人讀懂衬浑。

為了讓代碼更有美感,采取以下實(shí)踐會(huì)很有幫助:

用換行和列對(duì)齊來讓代碼更加整齊

選擇一個(gè)有意義的順序

把代碼分成”段落”

保持風(fēng)格一致性

用換行和列對(duì)齊來讓代碼更加整齊

有些時(shí)候放刨,我們可以利用換行和列對(duì)齊來讓代碼顯得更加整齊工秩。

換行

換行比較常用在函數(shù)或方法的參數(shù)比較多的時(shí)候。

使用換行:

- (void)requestWithUrl:(NSString*)url

? method:(NSString*)method

? ? ? ? ? ? ? ? params:(NSDictionary *)params

? ? ? ? ? ? ? success:(SuccessBlock)success

? ? ? ? ? ? ? failure:(FailuireBlock)failure{


}

不使用換行:

- (void)requestWithUrl:(NSString*)url method:(NSString*)method params:(NSDictionary *)params success:(SuccessBlock)success failure:(FailuireBlock)failure{


}

通過比較可以看出宏榕,如果不使用換行拓诸,就很難一眼看清楚都是用了什么參數(shù),而且代碼整體看上去整潔干凈了很多麻昼。

列對(duì)齊

在聲明一組變量的時(shí)候,由于每個(gè)變量名的長(zhǎng)度不同馋辈,導(dǎo)致了在變量名左側(cè)對(duì)齊的情況下抚芦,等號(hào)以及右側(cè)的內(nèi)容沒有對(duì)齊:

NSString *name = userInfo[@"name"];

NSString *sex = userInfo[@"sex"];

NSString *address = userInfo[@"address"];

而如果使用了列對(duì)齊的方法,讓等號(hào)以及右側(cè)的部分對(duì)齊的方式會(huì)使代碼看上去更加整潔:

NSString *name? ? = userInfo[@"name"];

NSString *sex? ? = userInfo[@"sex"];

NSString *address = userInfo[@"address"];

這二者的區(qū)別在條目數(shù)比較多以及變量名稱長(zhǎng)度相差較大的時(shí)候會(huì)更加明顯迈螟。

選擇一個(gè)有意義的順序

當(dāng)涉及到相同變量(屬性)組合的存取都存在的時(shí)候叉抡,最好以一個(gè)有意義的順序來排列它們:

讓變量的順序與對(duì)應(yīng)的HTML表單中字段的順序相匹配

從最重要到最不重要排序

按照字母排序

舉個(gè)例子:相同集合里的元素同時(shí)出現(xiàn)的時(shí)候最好保證每個(gè)元素出現(xiàn)順序是一致的。除了便于閱讀這個(gè)好處以外答毫,也有助于能發(fā)現(xiàn)漏掉的部分褥民,尤其當(dāng)元素很多的時(shí)候:

//給model賦值

model.name ? = dict["name"];

model.sex ? = dict["sex"]洗搂;

model.address = dict["address"]消返;

...


//拿到model來繪制UI

nameLabel.text? ? = model.name;

sexLabel.text? ? = model.sex;

addressLabel.text = model.address;

把代碼分成”段落”

在寫文章的時(shí)候载弄,為了能讓整個(gè)文章看起來結(jié)構(gòu)清晰,我們通常會(huì)把大段文字分成一個(gè)個(gè)小的段落撵颊,讓表達(dá)相同主旨的語(yǔ)言湊到一起宇攻,與其他主旨的內(nèi)容分隔開來。

而且除了讓讀者明確哪些內(nèi)容是表達(dá)同一主旨之外倡勇,把文章分為一個(gè)個(gè)段落的好處還有便于找到你的閱讀”腳印“逞刷,便于段落之間的導(dǎo)航;也可以讓你的閱讀具有一定的節(jié)奏感妻熊。

其實(shí)這些道理同樣適用于寫代碼:如果你可以把一個(gè)擁有好幾個(gè)步驟的大段函數(shù)夸浅,以空行+注釋的方法將每一個(gè)步驟區(qū)分開來,那么則會(huì)對(duì)讀者理解該函數(shù)的功能有極大的幫助扔役。這樣一來帆喇,代碼既能有一定的美感,也具備了可讀性厅目。其實(shí)可讀性又何嘗不是來自于規(guī)則番枚,富有美感的代碼呢?

BigFunction{


//step1:*****

? ? ....


//step2:*****

? ? ...


//step3:*****

? ? ....


}

保持風(fēng)格一致性

有些時(shí)候损敷,你的某些代碼風(fēng)格可能與大眾比較容易接受的風(fēng)格不太一樣葫笼。但是如果你在你自己所寫的代碼各處能夠保持你這種獨(dú)有的風(fēng)格,也是可以對(duì)代碼的可讀性有積極的幫助的拗馒。

比如一個(gè)比較經(jīng)典的代碼風(fēng)格問題:

if(condition){

}

or:

if(condition)

{

}

對(duì)于上面的兩種寫法路星,每個(gè)人對(duì)條件判斷右側(cè)的大括號(hào)的位置會(huì)有不同的看法。但是無(wú)論你堅(jiān)持的是哪一個(gè)诱桂,請(qǐng)?jiān)谀愕拇a里做到始終如一洋丐。因?yàn)槿绻心硯讉€(gè)特例的話,是非常影響代碼的閱讀體驗(yàn)的挥等。

我們要知道友绝,一個(gè)邏輯清晰的代碼也可以因?yàn)榱舭椎牟灰?guī)則,格式不對(duì)齊肝劲,順序混亂而讓人很難讀懂迁客,這是十分讓人痛心的事情。所以既然你的代碼在命名上辞槐,邏輯上已經(jīng)很優(yōu)秀了掷漱,就不妨再費(fèi)一點(diǎn)功夫把她打扮的漂漂亮亮的吧!

如何寫注釋

首先引用書中的一句話:

注釋的目的是盡量幫助讀者了解得和作者一樣多榄檬。

在你寫代碼的時(shí)候卜范,在腦海中可能會(huì)留下一些代碼里面很難體現(xiàn)出來的部分:這些部分在別人讀你的代碼的時(shí)候可能很難體會(huì)到。而這些“不對(duì)稱”的信息就是需要通過以注釋的方式來告訴閱讀代碼的人鹿榜。

想要寫出好的注釋海雪,就需要首先知道:

什么不能作為注釋

什么應(yīng)該作為注釋

什么不能作為注釋

我們都知道注釋占用了代碼的空間锦爵,而且實(shí)際上對(duì)程序本身的運(yùn)行毫無(wú)幫助,所以最好保證它是物有所值的喳魏。

不幸的是棉浸,有一些注釋是毫無(wú)價(jià)值的,它無(wú)情的占用了代碼間的空間刺彩,影響了閱讀代碼的人的閱讀效率迷郑,也浪費(fèi)了寫注釋的人的時(shí)間。這樣的注釋有以下兩種:

描述能立刻從代碼自身就能立刻理解的代碼意圖的注釋

給不好的命名添加的注釋

描述能立刻從代碼自身就能立刻理解的代碼意圖的注釋

//add params1 and params2 and return sum of them

- (int)addParam1:(int)param1 param2:(int)param2

上面這個(gè)例子舉的比較簡(jiǎn)單创倔,但反映的問題很明顯:這里面的注釋是完全不需要的嗡害,它的存在反而增加了閱讀代碼的人的工作量。因?yàn)樗麖姆椒涂梢择R上意會(huì)到這個(gè)函數(shù)的作用了畦攘。

給不好的命名添加的注釋

1- (void)

講完了注釋不應(yīng)該是什么內(nèi)容霸妹,現(xiàn)在講一下注釋應(yīng)該是什么樣的內(nèi)容:

什么應(yīng)該作為注釋

本書中介紹的注釋大概有以下幾種:

寫代碼時(shí)的思考

對(duì)代碼的評(píng)價(jià)

常量

全局觀的概述

?

寫代碼時(shí)的思考

你的代碼可能不是一蹴而就的,它的產(chǎn)生可能會(huì)需要一些思考的過程知押。然而很多時(shí)候代碼本身卻無(wú)法將這些思考表達(dá)出來叹螟,所以你就可能有必要通過注釋的方式來呈現(xiàn)你的思考,讓閱讀代碼的人知道這段代碼是哪些思考的結(jié)晶台盯,從而也讓讀者理解了這段代碼為什么這么寫罢绽。如果遇到了比你高明的高手,在他看到你的注釋之后興許會(huì)馬上設(shè)計(jì)出一套更加合適的方案静盅。

對(duì)代碼的評(píng)價(jià)

有些時(shí)候你知道你現(xiàn)在寫的代碼是個(gè)臨時(shí)的方案:它可能確實(shí)是解決當(dāng)前問題的一個(gè)方法良价,但是:

你知道同時(shí)它也存在著某些缺陷,甚至是陷阱

你不知道有其他的方案可以替代了

你知道有哪個(gè)方案可以替代但是由于時(shí)間的關(guān)系或者自身的能力無(wú)法實(shí)現(xiàn)

?

也可能你知道你現(xiàn)在實(shí)現(xiàn)的這個(gè)方案幾乎就是”完美的“蒿叠,因?yàn)槿绻褂昧似渌姆桨该鞴福赡軙?huì)消耗更多的資源等等。

對(duì)于上面這些情況市咽,你都有必要寫上幾個(gè)字作為注釋來誠(chéng)實(shí)的告訴閱讀你的這段代碼的人這段代碼的情況痊银,比如:

//該方案有一個(gè)很容易忽略的陷阱:****

//該方案是存在性能瓶頸,性能瓶頸在其中的**函數(shù)中

//該方案的性能可能并不是最好的施绎,因?yàn)槿绻褂媚衬乘惴ǖ脑捒赡軙?huì)好很多

常量

在定義常量的時(shí)候曼验,在其后面最好添加一個(gè)關(guān)于它是什么或者為什么它是這個(gè)值的原因。因?yàn)槌A客ǔJ遣粦?yīng)該被修改的粘姜,所以最好把這個(gè)常量為什么是這個(gè)值說明一下:

例如:

image_quality = 0.72 // 最佳的size/quanlity比率

retry_limit? = 4? ? // 服務(wù)器性能所允許的請(qǐng)求失敗的重試上限

全局觀的概述

對(duì)于一個(gè)剛加入團(tuán)隊(duì)的新人來說,除了團(tuán)隊(duì)文化熔酷,代碼規(guī)范以外孤紧,可能最需要了解的是當(dāng)前被分配到的項(xiàng)目的一些“全局觀”的認(rèn)識(shí):比如組織架構(gòu),類與類之間如何交互拒秘,數(shù)據(jù)如何保存号显,如何流動(dòng)臭猜,以及模塊的入口點(diǎn)等等。

有時(shí)僅僅添加了幾句話押蚤,可能就會(huì)讓新人迅速地了解當(dāng)前系統(tǒng)或者當(dāng)前類的結(jié)構(gòu)以及作用蔑歌,而且這些也同樣對(duì)開發(fā)過當(dāng)前系統(tǒng)的人員迅速回憶出之前開發(fā)的細(xì)節(jié)有很大幫助。

這些注釋可以在一個(gè)類的開頭(介紹這個(gè)類的職責(zé)揽碘,以及在整個(gè)系統(tǒng)中的角色)也可以在一個(gè)模塊入口處次屠。書中舉了一個(gè)關(guān)于這種注釋的例子:

1//這個(gè)文件包含了一些輔助函數(shù),尾門的文件系統(tǒng)提供了更便利的接口

再舉一個(gè)iOS開發(fā)里眾所周知的網(wǎng)絡(luò)框架AFNetworking的例子雳刺。在AFHTTPSessionManager的頭文件里說明了這個(gè)類的職責(zé):

1//AFHTTPSessionManager` is a subclass of `AFURLSessionManager` with convenience methods for making HTTP requests. When a `baseURL` is provided, requests made with the `GET` / `POST` / et al. convenience methods can be made with relative paths

在知道了什么不應(yīng)該是注釋以及什么應(yīng)該是注釋以后劫灶,我們來看一下一個(gè)真正合格的注釋應(yīng)該是什么樣子的:

注釋應(yīng)當(dāng)有很高的信息/空間率

也就是說,注釋應(yīng)該用最簡(jiǎn)短的話來最明確地表達(dá)掖桦。要做到這一點(diǎn)需要做的努力是:

讓注釋保持緊湊:盡量用最簡(jiǎn)潔的話來表達(dá)本昏,不應(yīng)該有重復(fù)的內(nèi)容

準(zhǔn)確地描述函數(shù)的行為:要把函數(shù)的具體行為準(zhǔn)確表達(dá)出來,不能停留在表明

用輸入/輸出的例子來說明特別的情況:有時(shí)相對(duì)于文字枪汪,可能用一個(gè)實(shí)際的參數(shù)和返回值就能立刻體現(xiàn)出函數(shù)的作用涌穆。而且有些特殊情況也可以通過這個(gè)方式來提醒閱讀代碼的人

聲明代碼的意圖:也就是說明這段代碼存在的意義,你為什么當(dāng)時(shí)是這么寫的原因

其實(shí)好的代碼是自解釋的雀久,由于其命名的合理以及架構(gòu)的清晰宿稀,幾乎不需要注釋來向閱讀代碼的人添加額外的信息,書中有一個(gè)公式可以很形象地表明一個(gè)好的代碼本身的重要性:

好代碼 > (壞代碼 + 注釋)

三. 控制流和邏輯的改進(jìn)

控制流在編碼中占據(jù)著很重要的位置岸啡,它往往代表著一些核心邏輯和算法原叮。因此,如果我們可以讓控制流變得看上去更加“自然”巡蘸,那么就會(huì)對(duì)閱讀代碼的人理解這些邏輯甚至是整個(gè)系統(tǒng)提供很大的幫助奋隶。

那么都有哪相關(guān)實(shí)踐呢?

使用符合人類自然語(yǔ)言的表達(dá)習(xí)慣

if/else語(yǔ)句塊的順序

使用return提前返回

使用符合人類自然語(yǔ)言的表達(dá)習(xí)慣

寫代碼也是一個(gè)表達(dá)的過程悦荒,雖然表現(xiàn)形式不同唯欣,但是如果我們能夠采用符合人類自然語(yǔ)言習(xí)慣的表達(dá)習(xí)慣來寫代碼,對(duì)閱讀代碼的人理解我們的代碼是很有幫助的搬味。

這里有兩個(gè)比較典型的情景:

條件語(yǔ)句中參數(shù)的順序

條件語(yǔ)句中的正負(fù)邏輯

條件語(yǔ)句中參數(shù)的順序:

首先比較一下下面兩段代碼境氢,哪一個(gè)更容易讀懂?

//code 1

if(length >10)

//code 2

if(10< length)

大家習(xí)慣上應(yīng)該會(huì)覺得code1容易讀懂碰纬。

再來看下面一個(gè)例子:

//code 3

if(received_number < standard_number)

//code 4

if( standard_number< received_number)

仔細(xì)看會(huì)發(fā)現(xiàn)萍聊,和上面那一組情況類似,大多數(shù)人還是會(huì)覺得code3更容易讀懂悦析。

那么code1 和 code3有什么共性呢寿桨?

它們的共性就是:左側(cè)都是被詢問的內(nèi)容(通常是一個(gè)變量);右側(cè)都是用來做比較的內(nèi)容(通常是一個(gè)常量)

這應(yīng)該是符合自然語(yǔ)言的一個(gè)順序强戴。比如我們一般會(huì)說“今天的氣溫大于20攝氏度”亭螟,而不習(xí)慣說“20攝氏度小于今天的氣溫”挡鞍。

條件語(yǔ)句中的正負(fù)邏輯:

在判斷一些正負(fù)邏輯的時(shí)候,建議使用if(result)而不是if(!result)预烙。

因?yàn)榇竽X比較容易處理正邏輯墨微,比如我們可能比較習(xí)慣說“某某某是個(gè)男人”,而不習(xí)慣說“某某某不是個(gè)女人”扁掸。如果我們使用了負(fù)邏輯翘县,大腦還要對(duì)它進(jìn)行取反,相當(dāng)于多做了一次處理也糊。

if/else語(yǔ)句塊的順序

在寫if/else語(yǔ)句的時(shí)候炼蹦,可能會(huì)有很多不同的互斥情況(好多個(gè)elseif)。那么這些互斥的情況可以遵循哪些順序呢狸剃?

先處理掉簡(jiǎn)單的情況掐隐,后處理復(fù)雜的情況:這樣有助于閱讀代碼的人循序漸進(jìn)地地理解你的邏輯,而不是一開始就吃掉一個(gè)胖子钞馁,耗費(fèi)不少精力虑省。

先處理特殊或者可疑的情況,后處理正常的情況:這樣有助于閱讀代碼的人會(huì)馬上看到當(dāng)前邏輯的邊界條件以及需要注意的地方僧凰。

使用return提前返回

在一個(gè)函數(shù)或是方法里探颈,可能有一些情況是比較特殊或者極端的,對(duì)結(jié)果的產(chǎn)生影響很大(甚至是終止繼續(xù)進(jìn)行)训措。如果存在這些情況伪节,我們應(yīng)該把他們寫在前面,用return來提前返回(或者返回需要返回的返回值)绩鸣。

這樣做的好處是可以減少if/else語(yǔ)句的嵌套怀大,也可以明確體現(xiàn)出:“哪些情況是引起異常的”。

再舉一個(gè)JSONModel里的例子呀闻,在initWithDictionary:error方法里面就有很多return操作化借,它們都體現(xiàn)出了“在什么情況下是不能成功將字典轉(zhuǎn)化為model對(duì)象”的;而且在方法的最后返回了對(duì)象捡多,說明如果到了這一步蓖康,則在轉(zhuǎn)化的過程中通過了層層考驗(yàn):

-(id)initWithDictionary:(NSDictionary*)dict error:(NSError**)err

{

? ? //check for nil input

? ? if (!dict) {

? ? ? ? if (err) *err = [JSONModelError errorInputIsNil];

? ? ? ? return nil;

? ? }

? ? //invalid input, just create empty instance

? ? if (![dict isKindOfClass:[NSDictionary class]]) {

? ? ? ? if (err) *err = [JSONModelError errorInvalidDataWithMessage:@"Attempt to initialize JSONModel object using initWithDictionary:error: but the dictionary parameter was not an 'NSDictionary'."];

? ? ? ? return nil;

? ? }

? ? //create a class instance

? ? self = [self init];

? ? if (!self) {

? ? ? ? //super init didn't succeed

? ? ? ? if (err) *err = [JSONModelError errorModelIsInvalid];

? ? ? ? return nil;

? ? }

? ? //check incoming data structure

? ? if (![self __doesDictionary:dict matchModelWithKeyMapper:self.__keyMapper error:err]) {

? ? ? ? return nil;

? ? }

? ? //import the data from a dictionary

? ? if (![self __importDictionary:dict withKeyMapper:self.__keyMapper validation:YES error:err]) {

? ? ? ? return nil;

? ? }

? ? //run any custom model validation

? ? if (![self validate:err]) {

? ? ? ? return nil;

? ? }

? ? //model is valid! yay!

? ? return self;

}

四. 代碼組織的改進(jìn)

關(guān)于代碼組織的改進(jìn),作者介紹了以下三種方法:

抽取出與程序主要目的“不相關(guān)的子邏輯”

重新組織代碼使它一次只做一件事情

借助自然語(yǔ)言描述來將想法變成代碼

抽取出與程序主要目的“不相關(guān)的子邏輯”

一個(gè)函數(shù)里面往往包含了其主邏輯與子邏輯垒手,我們應(yīng)該積極地發(fā)現(xiàn)并抽取出與主邏輯不相關(guān)的子邏輯蒜焊。具體思考的步驟是:

首先確認(rèn)這段代碼的高層次目標(biāo)是什么(主要目標(biāo))?

對(duì)于每一行代碼科贬,都要反思一下:“它是直接為了目標(biāo)而工作么山涡?”

如果答案是肯定的并且這些代碼占據(jù)著一定數(shù)量的行數(shù),我們就應(yīng)該將他們抽取到獨(dú)立的函數(shù)中。

比如某個(gè)函數(shù)的目標(biāo)是為了尋找距離某個(gè)商家最近的地鐵口鸭丛,那么這其中一定會(huì)重復(fù)出現(xiàn)一些計(jì)算兩組經(jīng)緯度之間距離的子邏輯。但是這些子邏輯的具體實(shí)現(xiàn)是不應(yīng)該出現(xiàn)在這個(gè)主函數(shù)里面的唐责,因?yàn)檫@些細(xì)節(jié)與這個(gè)主函數(shù)的目標(biāo)來講應(yīng)該是無(wú)關(guān)的鳞溉。

即是說,像這種類似于工具方法的函數(shù)其實(shí)是脫離于某個(gè)具體的需求的:它可以用在其他的主函數(shù)中鼠哥,也可以放在其他的項(xiàng)目里面熟菲。比如找到離運(yùn)動(dòng)場(chǎng)場(chǎng)最近的幾個(gè)公交站這個(gè)需求等等。

而像這種“抽取子邏輯或工具方法”的做法有什么好處呢朴恳?

提高了代碼的可讀性:將函數(shù)的調(diào)用與原來復(fù)雜的實(shí)現(xiàn)進(jìn)行替換抄罕,讓閱讀代碼的人很快能了解到該子邏輯的目的,讓他們把注意力放在更高層的主邏輯上于颖,而不會(huì)被子邏輯的實(shí)現(xiàn)(往往是復(fù)雜無(wú)味的)所影響呆贿。

便于修改和調(diào)試:因?yàn)橐粋€(gè)項(xiàng)目中可能會(huì)多次調(diào)用該子邏輯(計(jì)算距離,計(jì)算匯率森渐,保留小數(shù)點(diǎn))做入,當(dāng)業(yè)務(wù)需求發(fā)生改變的時(shí)候只需要改變這一處就可以了,而且調(diào)試起來也非常容易同衣。

便于測(cè)試:同理竟块,也是因?yàn)榭梢员欢啻握{(diào)用,在進(jìn)行測(cè)試的時(shí)候就比較有針對(duì)性耐齐。

從函數(shù)擴(kuò)大到項(xiàng)目浪秘,其實(shí)在一個(gè)項(xiàng)目里面,有很多東西不當(dāng)前這個(gè)項(xiàng)目所專有的埠况,它們是可以用在其他項(xiàng)目中的一些“通用代碼”耸携。這些通用代碼可以對(duì)當(dāng)前的項(xiàng)目一無(wú)所知,可以被用在其他任何項(xiàng)目中去询枚。

我們可以養(yǎng)成這個(gè)習(xí)慣违帆,“把一般代碼與項(xiàng)目專有代碼分開”,并不斷擴(kuò)大我們的通用代碼庫(kù)來解決更多的一般性問題金蜀。

重新組織代碼使它一次只做一件事情

一個(gè)比較大的函數(shù)或者功能可能由很多任務(wù)代碼組合而來刷后,在這個(gè)時(shí)候我們有必要將他們分為更小的函數(shù)來調(diào)用它們。

這樣做的好處是:我們可以清晰地看到這個(gè)功能是人如何一步一步完成的渊抄,而且拆分出來的小的函數(shù)或許也可以用在其他的地方尝胆。

所以如果你遇到了比較難讀懂的代碼,可以嘗試將它所做的所有任務(wù)列出來护桦『危可能馬上你就會(huì)發(fā)現(xiàn)這其中有些任務(wù)可以轉(zhuǎn)化成歹毒的函數(shù)或者類。而其他的部分可以簡(jiǎn)單的成為函數(shù)中的一個(gè)邏輯段落。

借助自然語(yǔ)言描述來將想法變成代碼

在設(shè)計(jì)一個(gè)解決方案之前贪染,如果你能夠用自然語(yǔ)言把問題說清楚會(huì)對(duì)整個(gè)設(shè)計(jì)非常有幫助缓呛。因?yàn)槿绻苯訌拇竽X中的想法轉(zhuǎn)化為代碼,可能會(huì)露掉一些東西杭隙。

但是如果你可以將整個(gè)問題和想法滴水不漏地說出來哟绊,就可能會(huì)發(fā)現(xiàn)一些之前沒有想到的問題。這樣可以不斷完善你的思路和設(shè)計(jì)痰憎。

五. 最后想說的

這本書從變量的命名到代碼的組織來講解了一些讓代碼的可讀性提高的一些實(shí)踐方法票髓。

其實(shí)筆者認(rèn)為代碼的可讀性也可以算作是一種溝通能力的一種體現(xiàn)。因?yàn)閷懘a的過程也可以被看做是寫代碼的人與閱讀代碼的人的一種溝通铣耘,只不過這個(gè)溝通是單向的:代碼的可讀性高洽沟,可以說明寫代碼的人思路清晰,而且TA可以明確蜗细,高效地把自己的思考和工作內(nèi)容以代碼的形式表述出來裆操。

所以筆者相信能寫出可讀性很高的代碼的人,TA對(duì)于自己的思考和想法的描述能力一定不會(huì)很差鳄乏。

如果你真的打算好好做編程這件事情跷车,建議你從最小的事情上做起:好好為你的變量起個(gè)名字。不要再以“我英語(yǔ)不好”或者“沒時(shí)間想名字”作為托辭橱野;把態(tài)度端正起來朽缴,平時(shí)多動(dòng)腦,多查字典水援,多看源碼筹淫,自然就會(huì)了正歼。

如果你連起個(gè)好的變量名都懶得查個(gè)字典,那你怎么證明你在遇到更難的問題的時(shí)候能夠以科學(xué)的態(tài)度解決它?

如果你連編程里這種最小的事情都不好好做隶糕,那你又怎么證明你對(duì)編程是有追求的呢岖研?


原文地址

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末丙挽,一起剝皮案震驚了整個(gè)濱河市扁达,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌惯豆,老刑警劉巖池磁,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異楷兽,居然都是意外死亡地熄,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門芯杀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來端考,“玉大人雅潭,你說我怎么就攤上這事∪刺兀” “怎么了扶供?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)核偿。 經(jīng)常有香客問我诚欠,道長(zhǎng),這世上最難降的妖魔是什么漾岳? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮粉寞,結(jié)果婚禮上尼荆,老公的妹妹穿的比我還像新娘。我一直安慰自己唧垦,他們只是感情好捅儒,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著振亮,像睡著了一般巧还。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上坊秸,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天麸祷,我揣著相機(jī)與錄音,去河邊找鬼褒搔。 笑死阶牍,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的星瘾。 我是一名探鬼主播走孽,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼琳状!你這毒婦竟也來了磕瓷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤念逞,失蹤者是張志新(化名)和其女友劉穎困食,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肮柜,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡陷舅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了审洞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片莱睁。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡待讳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出仰剿,到底是詐尸還是另有隱情创淡,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布南吮,位于F島的核電站琳彩,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏部凑。R本人自食惡果不足惜露乏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望涂邀。 院中可真熱鬧瘟仿,春花似錦、人聲如沸比勉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)浩聋。三九已至观蜗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間衣洁,已是汗流浹背墓捻。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留闸与,地道東北人毙替。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像践樱,于是被迫代替她去往敵國(guó)和親厂画。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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