編寫「可讀」代碼(練習(xí)makedown)

編寫可讀的代碼法焰,對于以代碼謀生的程序員而言秧荆,是一件極為重要的事。從某種角度來說埃仪,代碼最重要的功能是能夠被閱讀乙濒,其次才是能夠被正確執(zhí)行。一段無法正確執(zhí)行的代碼,也許會使項目延期幾天颁股,但它造成的危害只是暫時和輕微的么库,畢竟這種代碼無法通過測試并影響最終的產(chǎn)品;但是甘有,一段能夠正確執(zhí)行诉儒,但缺乏條理、難以閱讀的代碼亏掀,它造成的危害卻是深遠(yuǎn)和廣泛的:這種代碼會提高產(chǎn)品后續(xù)迭代和維護(hù)的成本忱反,影響產(chǎn)品的穩(wěn)定,破壞團(tuán)隊的團(tuán)結(jié)(霧)幌氮,除非我們花費數(shù)倍于編寫這段代碼的時間和精力缭受,來消除它對項目造成的負(fù)面影響。

在最近的工作和業(yè)余生活中该互,我對「如何寫出可讀的代碼」這個問題頗有一些具體的體會米者,不妨記錄下來吧。

JavaScript 是動態(tài)和弱類型的語言宇智,使用起來比較「輕松隨意」蔓搞,在 IE6 時代,輕松隨意的習(xí)慣確實不是什么大問題随橘,反而能節(jié)省時間喂分,提高出活兒的速度。但是机蔗,隨著當(dāng)下前端技術(shù)的快速發(fā)展蒲祈,前端項目規(guī)模的不斷膨脹,以往那種輕松隨意的編碼習(xí)慣萝嘁,已經(jīng)成為項目推進(jìn)的一大阻力梆掸。

這篇文章討論的是 ES6/7 代碼,不僅因為 ES6/7 已經(jīng)在大部分場合替代了 JavaScript牙言,還因為 ES6/7 中的很多特性也能幫助我們改善代碼的可讀性酸钦。

變量命名

變量命名是編寫可讀代碼的基礎(chǔ)。只有變量被賦予了一個合適的名字咱枉,才能表達(dá)出它在環(huán)境中的意義涵亏。

命名必須傳遞足夠的信息妈踊,形如 getData 這樣的函數(shù)命名就沒能提供足夠的信息,讀者也完全無法猜測這個函數(shù)會做出些什么事情畅形。而 fetchUserInfoAsync 也許就好很多抹沪,讀者至少會猜測出说铃,這個函數(shù)大約會遠(yuǎn)程地獲取用戶信息学赛;而且因為它有一個 Async 后綴河哑,讀者甚至能猜出這個函數(shù)會返回一個 Promise 對象。

命名的基礎(chǔ)

通常,我們使用名詞來命名對象河爹,使用動詞來命名函數(shù)。比如:


monkey.eat(banana); // the money eats a banana const apple = pick(tree); // pick an apple from the tree


這兩句代碼與自然語言(右側(cè)的注釋)很接近桐款,即使完全不了解編程的人也能看懂大概咸这。

有時候,我們需要表示某種集合概念魔眨,比如數(shù)組或哈希對象媳维。這時可以通過名詞的復(fù)數(shù)形式來表示,比如用 bananas 表示一個數(shù)組遏暴,這個數(shù)組的每一項都是一個 banana侄刽。如果需要特別強(qiáng)調(diào)這種集合的形式,也可以加上 List 或 Map 后綴來顯式表示出來朋凉,比如用 bananaList 表示數(shù)組州丹。

有些單詞的復(fù)數(shù)形式和單數(shù)形式相同,有些不可數(shù)的單詞沒有復(fù)數(shù)形式(比如 data杂彭,information)墓毒,這時我也會使用 List 等后綴來表示集合概念。

命名的上下文

變量都是處在上下文(作用域)之內(nèi)亲怠,變量的命名應(yīng)與上下文相契合所计,同一個變量,在不同的上下文中团秽,命名可以不同主胧。舉個例子,假設(shè)我們的程序需要管理一個動物園习勤,程序的代碼里有一個名為 feedAnimals 的函數(shù)來喂食動物園中的所有動物:


function feedAnimals(food, animals) { // ... // 上下文中有 bananas, peaches, monkey 變量 const banana = bananas.pop(); if (banana) { monkey.eat(banana); } else { const peach = peaches.pop(); monkey.eat(peach); } // ... }


負(fù)責(zé)喂食動物的函數(shù) feedAnimals 函數(shù)的主要邏輯就是:用各種食物把動物園里的各種動物喂飽踪栋。也許,每種動物能接受的食物種類不同姻报,也許己英,我們需要根據(jù)各種食物的庫存來決定每種動物最終分到的食物,總之在這個上下文中吴旋,我們需要關(guān)心食物的種類损肛,所以傳給 money.eat 方法的實參對象命名為 banana 或者 peach,代碼很清楚地表達(dá)出了它的關(guān)鍵邏輯:「猴子要么吃香蕉荣瑟,要么吃桃子(如果沒有香蕉了)」治拿。我們肯定不會這樣寫:


// 我們不會這樣寫 const food = bananas.pop(); if(food) { monkey.eat(food); } else { const food = peaches.pop(); monkey.eat(food); }


Monkey#eat 方法內(nèi)部就不一樣了,這個方法很可能是下面這樣的(假設(shè) eat 是 Monkey 的基類 Animal 的方法):


class Animal{ // ... eat(food) { this.hunger -= food.energy; } // ... } class Monkey extends Animal{ // ... }


如代碼所示笆焰,「吃」這個方法的核心邏輯就是根據(jù)食物的能量來減少動物(猴子)自身的饑餓度劫谅,至于究竟是吃了桃子還是香蕉,我們不關(guān)心,所以在這個方法的上下文中捏检,我們直接將表示食物的函數(shù)形參命名為 food荞驴。

想象一下,假設(shè)我們正在編寫某個函數(shù)贯城,即將寫一段公用邏輯熊楼,我們會選擇去寫一個新的功能函數(shù)來執(zhí)行這段公用邏輯。在編寫這個新的功能函數(shù)過程中能犯,往往會受到之前那個函數(shù)的影響鲫骗,變量的命名也是按照其在之前那個函數(shù)中的意義來的。雖然寫的時候不感覺有什么阻礙踩晶,但是讀者閱讀的單元是函數(shù)(他并不了解之前哪個函數(shù))执泰,會被深深地困擾。

嚴(yán)格遵循一種命名規(guī)范的收益

如果你能夠時刻按照某種嚴(yán)格的規(guī)則來命名變量和函數(shù)渡蜻,還能帶來一個潛在的好處术吝,那就是你再也不用記住哪些之前命名過(甚至其他人命名過)的變量或函數(shù)了。特定上下文中的特定含義只有一種命名方式晴楔,也就是說顿苇,只有一個名字。比如税弃,「獲取用戶信息」這個概念纪岁,就叫作 fetchUserInfomation,不管是在早晨還是傍晚则果,不管你是在公司還是家中幔翰,你都會將它命名為 fetchUserInfomation 而不是 getUserData。那么當(dāng)你再次需要使用這個變量時西壮,你根本不用翻閱之前的代碼或依賴 IDE 的代碼提示功能遗增,你只需要再命名一下「獲取用戶信息」這個概念,就可以得到 fetchUserInfomation 了款青,是不是很酷做修?

分支結(jié)構(gòu)

分支是代碼里最常見的結(jié)構(gòu),一段結(jié)構(gòu)清晰的代碼單元應(yīng)當(dāng)是像二叉樹一樣抡草,呈現(xiàn)下面的結(jié)構(gòu)饰及。


if (condition1) { if (condition2) { ... } else { ... } } else { if (condition3) { ... } else { ... } }


這種優(yōu)美的結(jié)構(gòu)能夠幫助我們在大腦中迅速繪制一張圖,便于我們在腦海中模擬代碼的執(zhí)行康震。但是燎含,我們大多數(shù)人都不會遵循上面這樣的結(jié)構(gòu)來寫分支代碼。以下是一些常見的腿短,在我看來可讀性比較差的分支語句的寫法:

不好的做法:在分支中 return


function foo() { if (condition) { // 分支1的邏輯 return; } // 分支2的邏輯 }


這種分支代碼很常見屏箍,而且往往分支 2 的邏輯是先寫的绘梦,也是函數(shù)的主要邏輯,分支 1 是后來對函數(shù)進(jìn)行修補(bǔ)的過程中產(chǎn)生的赴魁。這種分支代碼有一個很致命的問題卸奉,那就是,如果讀者沒有注意到分支1中的 return(我敢保證尚粘,在使用 IDE 把代碼折疊起來后择卦,沒人能第一時間注意到這個 return),就不會意識到后面一段代碼(分支 2)是有可能不會執(zhí)行的郎嫁。我的建議是,把分支 2 放到一個 else 語句塊中祈噪,代碼就會清晰可讀很多:


function foo() { if (condition) { // 分支 1 的邏輯 } else { // 分支 2 的邏輯 } }


如果某個分支是空的泽铛,我也傾向于留下一個空行,這個空行明確地告訴代碼的讀者辑鲤,如果走到這個 else盔腔,我什么都不會做。如果你不告訴讀者月褥,讀者就會產(chǎn)生懷疑弛随,并嘗試自己去弄明白。

不好的做法:多個條件復(fù)合


if (condition1 && condition2 && condition3) { // 分支1:做一些事情 } else { // 分支2:其他的事情 }


這種代碼也很常見:在若干條件同時滿足(或有任一滿足)的時候做一些主要的事情(分支1宁赤,也就是函數(shù)的主邏輯)舀透,否則就做一些次要的事情(分支2,比如拋異常决左,輸出日志等)愕够。雖然寫代碼的人知道什么是主要的事情,什么是次要的事情佛猛,但是代碼的讀者并不知道惑芭。讀者遇到這種代碼,就會產(chǎn)生困惑:分支2到底對應(yīng)了什么條件继找?

在上面這段代碼中遂跟,三種條件只要任意一個不成立就會執(zhí)行到分支 2,但這其實本質(zhì)上是多個分支:1)條件 1 不滿足婴渡,2)條件 1 滿足而條件 2 不滿足幻锁,3)條件 1 和 2 都滿足而條件 3 不滿足。如果我們籠統(tǒng)地使用同一段代碼來處理多個分支缩搅,那么就會增加閱讀者閱讀分支 2 時的負(fù)擔(dān)(需要考慮多個情況)越败。更可怕的是,如果后面需要增加一些額外的邏輯(比如硼瓣,在條件 1 成立且條件 2 不成立的時候多輸出一條日志)究飞,整個 if-else 都可能需要重構(gòu)置谦。

對這種場景,我通常這樣寫:


if (condition1) { if (condition2) { // 分支1:做一些事情 } else { // 分支2:其他的事情 } } else { // 分支3:其他的事情 }


即使分支 2 和分支 3 是完全一樣的亿傅,我也認(rèn)為有必要將其分開媒峡。雖然多了幾行代碼,收益卻是很客觀的葵擎。

萬事非絕對谅阿。對于一種情況,我不反對將多個條件復(fù)合起來酬滤,那就是當(dāng)被復(fù)合的多個條件聯(lián)系十分緊密的時候签餐,比如 if(foo && foo.bar)。

不好的做法:使用分支改變環(huán)境


let foo = someValue; if (condition) { foo = doSomethingTofoo(foo); } // 繼續(xù)使用 foo 做一些事情


這種風(fēng)格的代碼很容易出現(xiàn)在那些屢經(jīng)修補(bǔ)的代碼文件中盯串,很可能一開始是沒有這個 if 代碼塊的氯檐,后來發(fā)現(xiàn)了一個 bug,于是加上了這個 if 代碼塊体捏,在某些條件下對 foo 做一些特殊的處理冠摄。如果你希望項目在迭代過程中,風(fēng)險越積越高几缭,那么這個習(xí)慣絕對算得上「最佳實踐」了河泳。

事實上,這樣的「補(bǔ)丁」積累起來年栓,很快就會摧毀代碼的可讀性和可維護(hù)性拆挥。怎么說呢?當(dāng)我們在寫下上面這段代碼中的 if 分支以試圖修復(fù) bug 的時候韵洋,我們內(nèi)心存在這樣一個假設(shè):我們是知道程序在執(zhí)行到這一行時竿刁,foo 什么樣子的;但事實是搪缨,我們根本不知道食拜,因為在這一行之前,foo 很可能已經(jīng)被另一個人所寫的嘗試修復(fù)另一個 bug 的另一個 if 分支所篡改了副编。所以负甸,當(dāng)代碼出現(xiàn)問題的時候,我們應(yīng)當(dāng)完整地審視一段獨立的功能代碼(通常是一個函數(shù))痹届,并且多花一點時間來修復(fù)他呻待,比如:


const foo = condition ? doSomethingToFoo(someValue) : someValue;


我們看到,很多風(fēng)險都是在項目快速迭代的過程中積累下來的队腐。為了「快速」迭代蚕捉,在添加功能代碼的時候,我們有時候連函數(shù)這個最小單元的都不去了解柴淘,僅僅著眼于自己插入的那幾行迫淹,希望在那幾行中解決/hack掉所有問題秘通,這是十分不可取的。

我認(rèn)為敛熬,項目的迭代再快肺稀,其代碼質(zhì)量和可讀性都應(yīng)當(dāng)有一個底線。這個底線是应民,當(dāng)我們在修改代碼的時候话原,應(yīng)當(dāng)完整了解當(dāng)前修改的這個函數(shù)的邏輯,然后修改這個函數(shù)诲锹,以達(dá)到添加功能的目的繁仁。注意,這里的「修改一個函數(shù)」和「在函數(shù)某個位置添加幾行代碼」是不同的归园,在「修改一個函數(shù)」的時候改备,為了保證函數(shù)功能獨立,邏輯清晰蔓倍,不應(yīng)該畏懼在這個函數(shù)的任意位置增刪代碼。

函數(shù)

函數(shù)只做一件事情

有時盐捷,我們會自作聰明地寫出一些很「通用」的函數(shù)偶翅。比如,我們有可能寫出下面這樣一個獲取用戶信息的函數(shù) fetchUserInfo:其邏輯是:

  1. 當(dāng)傳入的參數(shù)是用戶ID(字符串)時碉渡,返回單個用戶數(shù)據(jù)聚谁;
  2. 而傳入的參數(shù)是用戶ID的列表(數(shù)組)時,返回一個數(shù)組滞诺,其中的每一項是一個用戶的數(shù)據(jù)形导。

async function fetchUserInfo(id) { const isSingle = typeof idList === 'string'; const idList = isSingle ? [id] : id; const result = await request.post('/api/userInfo', {idList}); return isSingle ? result[0] : result; }

// 可以這樣調(diào)用 const userList = await fetchUserInfo(['1011', '1013']); // 也可以這樣調(diào)用 const user = await fetchUserInfo('1017');


這個函數(shù)能夠做兩件事:1)獲取多個用戶的數(shù)據(jù)列表;2)獲取單個用戶的數(shù)據(jù)习霹。在項目的其他地方調(diào)用 fetchUserInfo 函數(shù)時朵耕,也許我們確實能感到「方便」了一些。但是淋叶,代碼的讀者一定不會有相同的體會阎曹,當(dāng)讀者在某處讀到 fetchUserInfo(['1011', '1013']) 這句調(diào)用的代碼時,他就會立刻對 fetchUserInfo 產(chǎn)生「第一印象」:這個函數(shù)需要傳入用戶ID數(shù)組煞檩;當(dāng)他讀到另外一種調(diào)用形式時处嫌,他一定會懷疑自己之前是不是眼睛花了。讀者并不了解背后的「潛規(guī)則」斟湃,除非規(guī)則是預(yù)先設(shè)計好并且及時地更新到文檔中熏迹。總之凝赛,我們絕不該一時興起就寫出上面這種函數(shù)注暗。

遵循一個函數(shù)只做一件事的原則坛缕,我們可以將上述功能拆成兩個函數(shù)fetchMultipleUser 和 fetchSingleUser 來實現(xiàn)。在需要獲取用戶數(shù)據(jù)時友存,只需要選擇調(diào)用其中的一個函數(shù)祷膳。


async function fetchMultipleUser(idList) { return await request.post('/api/users/', {idList}); }

async function fetchSingleUser(id) { return await fetchMultipleUser([id])[0]; }


上述改良不僅改善了代碼的可讀性,也改善了可維護(hù)性屡立。舉個例子直晨,假設(shè)隨著項目的迭代,獲取單一用戶信息的需求不再存在了膨俐。

  • 如果是改良前勇皇,我們會刪掉那些「傳入單個用戶ID來調(diào)用 fetchUserInfo」的代碼,同時保留剩下的那些「傳入多個用戶ID調(diào)用 fetchUserInfo」的代碼焚刺, 但是 fetchUserInfo 函數(shù)幾乎一定不會被更改敛摘。這樣,函數(shù)內(nèi)部 isSingle 為 true 的分支乳愉,就留在了代碼中兄淫,成了永遠(yuǎn)都不會執(zhí)行的「臟代碼」,誰愿意看到自己的項目中充斥著永遠(yuǎn)不會執(zhí)行的代碼呢蔓姚?
    
  • 對于改良后的代碼捕虽,我們(也許借助IDE)能夠輕松檢測到 fetchSingleUser 已經(jīng)不會被調(diào)用了,然后放心大膽地直接刪掉這個函數(shù)坡脐。
    

那么泄私,如何界定某個函數(shù)做的是不是一件事情?我的經(jīng)驗是這樣:如果一個函數(shù)的參數(shù)僅僅包含輸入數(shù)據(jù)(交給函數(shù)處理的數(shù)據(jù))备闲,而沒有混雜或暗含有指令(以某種約定的方式告訴函數(shù)該怎么處理數(shù)據(jù))晌端,那么函數(shù)所做的應(yīng)當(dāng)就是一件事情。比如說恬砂,改良前的 fetchUserInfo 函數(shù)的參數(shù)是「多個用戶的ID數(shù)組或單個用戶的ID」咧纠,這個「或」字其實就暗含了某種指令。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末觉既,一起剝皮案震驚了整個濱河市惧盹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瞪讼,老刑警劉巖钧椰,帶你破解...
    沈念sama閱讀 211,348評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異符欠,居然都是意外死亡嫡霞,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評論 2 385
  • 文/潘曉璐 我一進(jìn)店門希柿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诊沪,“玉大人养筒,你說我怎么就攤上這事《艘Γ” “怎么了晕粪?”我有些...
    開封第一講書人閱讀 156,936評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長渐裸。 經(jīng)常有香客問我巫湘,道長,這世上最難降的妖魔是什么昏鹃? 我笑而不...
    開封第一講書人閱讀 56,427評論 1 283
  • 正文 為了忘掉前任尚氛,我火速辦了婚禮,結(jié)果婚禮上洞渤,老公的妹妹穿的比我還像新娘阅嘶。我一直安慰自己,他們只是感情好载迄,可當(dāng)我...
    茶點故事閱讀 65,467評論 6 385
  • 文/花漫 我一把揭開白布讯柔。 她就那樣靜靜地躺著,像睡著了一般护昧。 火紅的嫁衣襯著肌膚如雪磷杏。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,785評論 1 290
  • 那天捏卓,我揣著相機(jī)與錄音,去河邊找鬼慈格。 笑死怠晴,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的浴捆。 我是一名探鬼主播蒜田,決...
    沈念sama閱讀 38,931評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼选泻!你這毒婦竟也來了冲粤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,696評論 0 266
  • 序言:老撾萬榮一對情侶失蹤页眯,失蹤者是張志新(化名)和其女友劉穎梯捕,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體窝撵,經(jīng)...
    沈念sama閱讀 44,141評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡傀顾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,483評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了碌奉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片短曾。...
    茶點故事閱讀 38,625評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡寒砖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嫉拐,到底是詐尸還是另有隱情哩都,我是刑警寧澤,帶...
    沈念sama閱讀 34,291評論 4 329
  • 正文 年R本政府宣布婉徘,位于F島的核電站漠嵌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏判哥。R本人自食惡果不足惜献雅,卻給世界環(huán)境...
    茶點故事閱讀 39,892評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望塌计。 院中可真熱鬧挺身,春花似錦、人聲如沸锌仅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽热芹。三九已至贱傀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間伊脓,已是汗流浹背府寒。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留报腔,地道東北人株搔。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像纯蛾,于是被迫代替她去往敵國和親纤房。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,492評論 2 348

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

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉翻诉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,690評論 0 9
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理炮姨,服務(wù)發(fā)現(xiàn),斷路器碰煌,智...
    卡卡羅2017閱讀 134,629評論 18 139
  • 原文: https://github.com/ecomfe/spec/blob/master/javascript...
    zock閱讀 3,371評論 2 36
  • 1. 1G 模擬 制式 大哥大 智能語音通話 2G CSM ,CDMA 收發(fā)短信和郵件 2.5G ...
    雷一凡閱讀 314評論 0 0
  • 我是誰 Who I am 舒岸,最近國內(nèi)各種靈修課程體系玲瑯滿目,我16年4月份到現(xiàn)在也參加了不少老師的課程芦圾。老師說:...
    Winner33閱讀 357評論 0 6