程序員的自我修養(yǎng):如何寫出高質(zhì)量的代碼
本文作者是螞蟻集團(tuán)前端工程師梧闕,介紹了代碼質(zhì)量標(biāo)準(zhǔn)和設(shè)計(jì)原則,并從編碼和設(shè)計(jì)的角度列舉了一些常見的壞味道的代碼,最后對(duì)壞味道的代碼進(jìn)行了重構(gòu),歡迎一起來探討~

??我們?yōu)槭裁葱枰哔|(zhì)量的代碼?
你是否有過為了修改一處簡單的功能,看了半天源代碼而無從下手的時(shí)候?
你是否遇到過前端未正確處理異常,導(dǎo)致頁面白屏的時(shí)候?
你是否遇到需要重構(gòu)一大堆代碼,卻發(fā)現(xiàn)沒有任何測試用例,只能靠人肉回歸的時(shí)候?
這三個(gè)常見的場景分別對(duì)應(yīng)代碼質(zhì)量衡量指標(biāo)中的可維護(hù)性、魯棒性和可測試性。因此代碼質(zhì)量并不是只是與維護(hù)者的心情息息相關(guān),與軟件的交付質(zhì)量也是密不可分。
??知名程序員如何看待高質(zhì)量的代碼
Bjarne Stroustrup
C++語言發(fā)明者,《C++程序設(shè)計(jì)語言》作者
我喜歡優(yōu)雅和高效的代碼,代碼邏輯應(yīng)當(dāng)直截了當(dāng),叫缺陷難以隱藏;盡量減少依賴關(guān)系,使之便于維護(hù);依據(jù)某種分層戰(zhàn)略完善錯(cuò)誤處理代碼;性能調(diào)至最優(yōu),省得引誘別人做沒規(guī)矩的優(yōu)化搞出一堆混亂來。整潔的代碼只做好一件事。
Grady Booch
UML 創(chuàng)始人,《面向?qū)ο蠓治雠c設(shè)計(jì)》作者
整潔的代碼簡單直接。整潔的代碼如同優(yōu)美的散文。整潔的代碼從不隱藏設(shè)計(jì)者的意圖,充滿了干凈利落的抽象和直截了當(dāng)?shù)目刂普Z句。
Ward Cunningham
Wiki 發(fā)明者,極限編程創(chuàng)始人之一
如果每個(gè)例程都讓你感到深合己意,那就是整潔代碼。如果代碼讓編程語言看上去像是專為解決那個(gè)問題而存在,就可以稱之為漂亮的代碼。
??代碼質(zhì)量指標(biāo)
? 可維護(hù)性
系統(tǒng)的可維護(hù)性是衡量一個(gè)系統(tǒng)的可修復(fù)(恢復(fù))性和可改進(jìn)性的難易程度。所謂可修復(fù)性是指在系統(tǒng)發(fā)生故障后能夠排除(或抑制)故障予以修復(fù),并返回到原來正常運(yùn)行狀態(tài)的可能性。而可改進(jìn)性則是系統(tǒng)具有接受對(duì)現(xiàn)有功能的改進(jìn),增加新功能的可能性。
可讀性
羅伯特·根寧公式
句子的形成。句子越單純,其可讀性越大。
迷霧系數(shù) (Fog index)。這是指詞匯抽象和艱奧難懂的程度。因此,迷霧系數(shù)越大,其可讀性越小。
人情味成分。新聞中含人情味成分越多,其可讀性越大。
新聞可讀性的研究是隨著西方報(bào)業(yè)競爭興起的,其目的在于改進(jìn)新聞寫作,以求擴(kuò)大發(fā)行量。除去第三點(diǎn)人情味成分,前兩點(diǎn)對(duì)于改進(jìn)代碼的可讀性也有著普適的借鑒意義。
名副其實(shí)
如果我們的命名需要使用注釋來補(bǔ)充,那就不算是名副其實(shí),例如下面這個(gè)例子:
let?d;?//?消逝的時(shí)間,以日計(jì)
我們應(yīng)該指明了計(jì)量對(duì)象和計(jì)量單位的名稱:
避免誤導(dǎo)
保證命名的類型準(zhǔn)確性,例如是一個(gè)哈希表就不要使用 List 結(jié)尾,數(shù)組變量就不要使用單數(shù)形式的命名。
避免過于相似的命名: 例如 ControllerForEfficientHandlingOfStrings 和 ControllerForEfficientStorageOfStrings
避免無意義的區(qū)分: 例如 getActiveAccount() 和 getActiveAccountInfo()
保持語義一致: 例如多個(gè)類中都有 add 方法,該方法傳入兩個(gè)集合參數(shù)而獲得一個(gè)新的集合。而現(xiàn)在新增加了一個(gè)類,有個(gè)函數(shù)是將單個(gè)參數(shù)放入集合中,用 add 來命名語義就不一致了,應(yīng)當(dāng)使用 append 之類的詞來命名。
可擴(kuò)展性與可復(fù)用性
可擴(kuò)展性在軟件工程領(lǐng)域是指:設(shè)計(jì)良好的代碼允許更多的功能在必要時(shí)可以被插入到適當(dāng)?shù)奈恢弥小_@樣做的目的的是為了應(yīng)對(duì)未來可能需要進(jìn)行的修改,而造成代碼被過度工程化地開發(fā)。
為了實(shí)現(xiàn)這一目標(biāo),靈活的設(shè)計(jì)和封裝的細(xì)粒度必不可少。具體可以查看下文中的單一職責(zé)原則與開發(fā)-封閉原則一節(jié)。
? 魯棒性(健壯性)
魯棒是Robust的音譯,也就是健壯和強(qiáng)壯的意思。它也是在異常和危險(xiǎn)情況下系統(tǒng)生存的能力。比如說,計(jì)算機(jī)軟件在輸入錯(cuò)誤、磁盤故障、網(wǎng)絡(luò)過載或有意攻擊情況下,能否不死機(jī)、不崩潰,就是該軟件的魯棒性。所謂“魯棒性”,也是指控制系統(tǒng)在一定(結(jié)構(gòu),大?。┑膮?shù)攝動(dòng)下,維持其它某些性能的特性。根據(jù)對(duì)性能的不同定義,可分為穩(wěn)定魯棒性和性能魯棒性。
穩(wěn)定
程序的穩(wěn)定性與邊界條件和異常處理息息相關(guān)。對(duì)于邊界條件的處理可以查看下文中可測試性與完整性一節(jié)。本節(jié)著重講述一下防御性編程和異常處理的部分。
防御性編程
防御性編程,是防御式設(shè)計(jì)的一種具體體現(xiàn),它是為了保證對(duì)程序的不可預(yù)見的使用不會(huì)造成程序功能上的損壞。
防御性編程的幾大原則:
使用好的編碼風(fēng)格和合理的設(shè)計(jì)
不要倉促的編寫代碼:欲速則不達(dá),想清楚你要輸入的是什么,在寫每一行時(shí)都三思而后行。可能會(huì)出現(xiàn)什么樣的錯(cuò)誤?你是否已經(jīng)考慮了所有可能出現(xiàn)的邏輯分支?
不要相信任何人: ?包括用戶的輸入和后端的返回值。一個(gè)殘酷的事實(shí)是,他們不會(huì)永遠(yuǎn)給到前端程序正確的輸入。
編碼的目標(biāo)要清晰,而不是簡潔:不要為了炫技而使用一些可讀性低的簡潔寫法,如何平衡簡潔和可讀性是一個(gè)程序員必修的課題。
舉個(gè)具體的例子,例如后端返回值預(yù)期返回一個(gè)對(duì)象數(shù)組,但是返回了一個(gè)非空的空對(duì)象,這時(shí)我們的短路運(yùn)算符其實(shí)并不能起到類型保護(hù)的作用:
對(duì)于這樣的數(shù)據(jù), 我們可以使用 lodash 的 toArray 方法做一次類型轉(zhuǎn)換來保證類型安全:
異常處理
前端的異常處理主要需要著眼于異常發(fā)生時(shí),盡可能的保證頁面功能模塊的可用,將異常的影響范圍盡可能的縮小。
對(duì)于所有可能出現(xiàn)異常的代碼,加上 try-catch 語句: 未被捕獲的異常,自異常的那一行后續(xù)的代碼都不會(huì)繼續(xù)執(zhí)行,影響面是不可控的。
加上 ErrorBoundary: ErrorBoundary 組件使得頁面白屏?xí)r能夠展示降級(jí) (fallback) 組件,不管怎么說用戶對(duì)于一個(gè)可交互的異常頁的心理預(yù)期, 是遠(yuǎn)勝于一個(gè)空白頁的。
對(duì)于視圖中使用到的變量, 需要保證取值不會(huì)導(dǎo)致異常: ?例如對(duì)一個(gè)字符串變量執(zhí)行?
s.length
?操作,當(dāng) s 變量不存在 length 屬性時(shí),就會(huì)導(dǎo)致視圖渲染異常。我們可以選擇使用可選鏈來保證安全性?s?.length
性能
通用優(yōu)化
對(duì)于性能優(yōu)化,既有一些行業(yè)通用的方法論,例如空間換時(shí)間、并行、減少計(jì)算量等。例如一些查找操作可以用哈希表來做時(shí)間復(fù)雜度的優(yōu)化:
編程語言寫法優(yōu)化
同時(shí)也有一些編程語言寫法上的性能優(yōu)化, 對(duì)于 JavaScript 而言,我們可以使用一些在線 jsperf 網(wǎng)站測試不同寫法的性能區(qū)別, 例如:

Date.now()
?與?+new Date()?
兩種不同的產(chǎn)生當(dāng)前時(shí)間戳的寫法,在筆者的 MacBook Pro (i7) 上使用 Chrome 101 版本執(zhí)行效率大致有 56% 的差距。
前端優(yōu)化
對(duì)于前端而言, ?另一個(gè)主要的性能優(yōu)化場景就是盡可能的減少組件的重新渲染,尤其是存在高昂計(jì)算成本的組件。以 React 為例,舉一個(gè)最為常見的性能優(yōu)化案例:
在線示例
每次 color 改變都會(huì)導(dǎo)致重新渲染,白白浪費(fèi) 100 ms 的渲染時(shí)間。因此我們需要縮小 color 這個(gè)狀態(tài)變動(dòng)導(dǎo)致 render 的影響范圍:
在線示例
這樣一來,color 的變化就不會(huì)導(dǎo)致和的重新渲染了。React 團(tuán)隊(duì)為了讓我們將組件粒度拆的盡可能的細(xì),不惜以性能為代價(jià),真是用心良苦(霧)
下面我們?cè)賹?duì)上面的場景進(jìn)行擴(kuò)展,在頂層組件和子組件中都使用了狀態(tài) color ,但仍然不關(guān)心它:
這時(shí)我們可以將作為一個(gè)插槽傳入包裝后的頂層組件, color 的變化就不會(huì)導(dǎo)致的重新渲染了。
? 可測試性與完整性
測試金字塔與單元測試

對(duì)于軟件測試而言,存在著單元測試、集成測試、組件測試、E2E測試和探索性測試,越往后編寫測試用例的成本就越高,因此用例數(shù)量也會(huì)隨之減少。對(duì)于大多數(shù)的場景我們只要寫好單元測試就可以滿足我們的需求了。
集團(tuán)的開發(fā)規(guī)約中描述了好的單測的特征, 被稱為?AIR 原則
A:Automatic(自動(dòng)化):單元測試應(yīng)該是全自動(dòng)執(zhí)行的,并且非交互式的。
I:Independent(獨(dú)立性):為了保證單元測試穩(wěn)定可靠且便于維護(hù),單元測試用例之間決不能互相調(diào)用,也不能依賴執(zhí)行的先后次序。
R:Repeatable(可重復(fù)):單元測試通常會(huì)被放到持續(xù)集成中,每次有代碼 check in 時(shí)單元測試都會(huì)被執(zhí)行。如果單測對(duì)外部環(huán)境(網(wǎng)絡(luò)、服務(wù)、中間件等)有依賴,容易導(dǎo)致持續(xù)集成機(jī)制的不可用。
其中的 A 和 R 可以由我們的測試框架來保證,而獨(dú)立性則需要我們自己保證。例如我們應(yīng)當(dāng)在測試套件的beforeEach 生命周期里做變量的初始化, 從而保證測試的獨(dú)立性:
另外對(duì)于 React 組件的單元測試,讀者可以翻閱 Bigfish 文檔中的 React 單元測試入門 一文, 本文就不展開贅述。
完整性
好的測試用例是應(yīng)當(dāng)具備完整性的,包括功能測試、邊界測試和反向(負(fù)面)測試的完整性。
功能測試
檢驗(yàn)對(duì)于正確的輸入以及極端情況的輸入,程序運(yùn)行的結(jié)果是否符合預(yù)期。
例如對(duì)于傳入數(shù)字的場景傳入一個(gè)超過32位的大數(shù)。
邊界測試
檢驗(yàn)是否對(duì)邊界條件進(jìn)行處理。下面是幾種常見的邊界條件:
遞歸的終止條件
循環(huán)的開始與終止條件
給定數(shù)字區(qū)間, 輸入?yún)^(qū)間邊緣的值
反向(負(fù)面)測試
檢驗(yàn)對(duì)于錯(cuò)誤的輸入, 程序運(yùn)行的結(jié)果是否符合預(yù)期。
要求非空的輸入,傳入一個(gè)?
null
?值要求是數(shù)組的輸入, 傳入一個(gè)空對(duì)象
{}
??設(shè)計(jì)原則
? SOLID
SOLID 是面向?qū)ο笤O(shè)計(jì)五個(gè)最為重要原則的首字母簡寫,是數(shù)十年軟件工程經(jīng)驗(yàn)來之不易的成果。
單一職責(zé)原則
The Single Responsibility Principle, 簡稱 SRP。這一設(shè)計(jì)原則的主要思想是一個(gè)模塊只負(fù)責(zé)一個(gè)職責(zé)。
例如下面這個(gè)調(diào)制解調(diào)器的類就違反了 SRP:
它既負(fù)責(zé)了連接管理又負(fù)責(zé)了數(shù)據(jù)通信, ?出于提高代碼內(nèi)聚性(模塊組成元素之間的功能相關(guān)性),降低耦合性的考量,我們應(yīng)該將其拆分為兩個(gè)類, 如果真的有必要我們?cè)賹⑵浣M合成一個(gè)超類:

開放-封閉原則
The Open-Closed Principle, 簡稱 OCP。這一設(shè)計(jì)原則的主要思想是拓展軟件實(shí)體時(shí)(類、模塊、函數(shù)等),不應(yīng)當(dāng)修改原本正常運(yùn)行的代碼。即對(duì)于拓展是開放的,對(duì)于更改是封閉的。
例如下面這個(gè)繪制所有形狀的函數(shù)就違反了 OCP:
每當(dāng)我們需要新增一個(gè)形狀的時(shí)候,我們都需要修改一次 drawAllShapes 函數(shù),顯然違反了"對(duì)于更改是封閉的"這一原則。
我們可以改變思路,將繪制函數(shù)內(nèi)聚在各個(gè)形狀之中,順便為繪制所有形狀的函數(shù)添加 hooks 以應(yīng)對(duì)后續(xù)的拓展:

里氏替換原則
The Liskov Substitution Principle, 簡稱 LSP。這一設(shè)計(jì)原則的主要思想是子類必須能夠替換掉他的基類。
例如下面這個(gè) Square 和 Rectangle 類的例子就違反了 LSP:
Square 作為子類并不能替換掉基類 Rectangle, ?Square 在修改高度的時(shí)候會(huì)同時(shí)修改寬度以保持正方形的特性, 對(duì)于下面這個(gè)函數(shù),傳入一個(gè)矩形和正方形會(huì)導(dǎo)致不一樣的運(yùn)行結(jié)果:

依賴倒置原則
The Dependency Inversion Principle, 簡稱 DIP。這一設(shè)計(jì)原則的主要思想是高層模塊不應(yīng)當(dāng)依賴于低層模塊,兩者都應(yīng)該依賴于抽象。
例如下面這個(gè) Button 類就違反了 DIP:
lamp 耦合在了 Button 中,這意味著 Lamp 類改變時(shí),Button 類會(huì)受到影響。此外,想用 Button 類來控制一個(gè) Motor 對(duì)象是不可能的。因此我們需要將 Lamp 類 抽象成一個(gè) SwitchableDevice 類, ?然后將 device 實(shí)例在實(shí)例化時(shí)動(dòng)態(tài)注入:
依賴倒置在后端生態(tài)中十分常見,接觸過 Java 的同學(xué)應(yīng)該都會(huì)聽過依賴注入(Dependency Injection,簡稱DI)和控制反轉(zhuǎn)(Inversion of Control, 簡稱 IoC)的概念,IoC 就是 DIP 在工程應(yīng)用中的叫法,DI 就是具體實(shí)現(xiàn) DIP 的方式。

接口隔離原則
The Interface Segregation Principle, 簡稱 ISP。?這一設(shè)計(jì)原則的主要思想是如果類的接口不是內(nèi)聚的,就應(yīng)該被拆解為多個(gè)。類似于接口(架構(gòu)設(shè)計(jì))層面的 SRP。
例如下面這個(gè) IOrder 接口就違反了 ISP:
上面的 apply、approve、end 還算是訂單通用的接口,而 changeSupplier 顯然是生產(chǎn)訂單才會(huì)用到的接口,changeShop 銷售訂單才會(huì)用到。因此我們應(yīng)該將其拆解開, ?需要使用時(shí)可以利用多繼承將它們組合到一起:

? KISS
KISS 是“Keep it Simple and Stupid”的首字母簡寫,這個(gè)詞最初被美國海軍使用,后來被廣泛應(yīng)用于其他領(lǐng)域,包括軟件工程。這一原則的主要思想是簡潔而清晰的設(shè)計(jì)往往會(huì)帶來更高的可維護(hù)性和可測試性。
??壞味道的代碼
講完了代碼質(zhì)量指標(biāo)和設(shè)計(jì)原則,下面從編碼和設(shè)計(jì)的角度列舉了一些常見的壞味道的代碼:
? 編碼
神秘命名
There are only two hard things in Computer Science: cache invalidation and naming things. -- Phil Karlton
命名作為計(jì)算機(jī)科學(xué)的兩大難題之一,一直困擾著眾多的程序員們。為了趕進(jìn)度有些人可能會(huì)選擇使用 a1, a2 這樣隨意的命名,這樣的變量不加上注釋對(duì)于維護(hù)者無疑是一場災(zāi)難,而一個(gè)好的命名顯然無需注釋就能讓閱讀者知道它的意義。而且如果你想不出一個(gè)好名字,背后往往潛藏著更深的設(shè)計(jì)問題。為一個(gè)惱人的名字所付出的糾結(jié),常常能推動(dòng)我們對(duì)代碼進(jìn)行精簡。
重復(fù)代碼與數(shù)據(jù)泥團(tuán)
這樣的壞味道在項(xiàng)目中是非常常見的,程序員們對(duì)于自己復(fù)制粘貼的行為總是可以找出各式各樣的理由來推脫。但是重復(fù)代碼是很難維護(hù)的,一旦有重復(fù)代碼存在,閱讀這些代碼就得加倍仔細(xì),留意其間細(xì)微的差異,這會(huì)給代碼修改造成很大的困擾。你常??梢栽诤芏嗟胤娇吹较嗤膸醉?xiàng)數(shù)據(jù):兩個(gè)類中相同的字段、許多函數(shù)簽名中相同的參數(shù), ?這些數(shù)據(jù)項(xiàng)被形象的稱為數(shù)據(jù)泥團(tuán)。不同于大段的重復(fù)代碼,人們總是傾向于保留數(shù)據(jù)泥團(tuán), 但是這些總是綁在一起出現(xiàn)的數(shù)據(jù)真應(yīng)該擁有屬于它們自己的對(duì)象。
過長函數(shù)/參數(shù)列表/類
早在編程的洪荒年代,程序員們就已認(rèn)識(shí)到: 代碼越長就越難理解。另外不只是可讀性有所欠缺,代碼的可復(fù)用性、可擴(kuò)展性和可測試性都會(huì)隨著長度的增加而降低。
Dead Code
注釋掉與不再使用的代碼會(huì)隨著時(shí)間推移越來越與系統(tǒng)無關(guān),沒有人知道他有多舊,也沒有人知道它有沒有意義。沒有人會(huì)刪除它,因?yàn)榇蠹叶技僭O(shè)別人需要它或是有進(jìn)一步的計(jì)劃。因此對(duì)于自己導(dǎo)致的 Dead Code 應(yīng)當(dāng)及時(shí)刪除,不用擔(dān)心,源代碼控制系統(tǒng)會(huì)記得它。
? 設(shè)計(jì)
發(fā)散式變化與霰彈式修改
新加一個(gè)功能必須修改很多個(gè)分散在各個(gè)文件中的代碼才能完成,這樣的壞味道被形象的稱之為霰彈式修改。如果需要修改的代碼散布四處,不但很難找到它們,也很容易錯(cuò)過某個(gè)重要的修改。新加一個(gè)功能必須修改另一個(gè)或幾個(gè)相關(guān)模塊的代碼才能完成,這樣的壞味道被稱為發(fā)散式變化。兩者都是非常不合理的設(shè)計(jì),這樣的設(shè)計(jì)顯然違反了我們下文將會(huì)提到的開放-封閉原則。
夸夸其談通用性
有人說“哦,我想我們總有一天需要做這件事”,并因此企圖用各式各樣的鉤子和特殊情況來處理一些非必要的事情。但是這么做的后果往往是導(dǎo)致系統(tǒng)更難理解和維護(hù)。
內(nèi)幕交易
模塊間的數(shù)據(jù)交換邏輯可能散落在各自不同文件的模塊中,就像私底下進(jìn)行的內(nèi)幕交易。對(duì)于這些不可避免的數(shù)據(jù)交換我們應(yīng)當(dāng)將它們放在一處共同的地方,減少它們私下的交流。
依戀情結(jié)
一個(gè)函數(shù)跟另一個(gè)模塊中的函數(shù)或者數(shù)據(jù)交流格外頻繁,遠(yuǎn)勝于在自己所處模塊內(nèi)部的交流,這就是依戀情結(jié)的典型情況。這時(shí)我們就應(yīng)當(dāng)將函數(shù)移動(dòng)到它的依戀模塊中。
過度使用委托
如果某個(gè)類的接口有一半的函數(shù)都委托給其他類,這樣就屬于過度運(yùn)用委托。這時(shí)應(yīng)該使用移除中間人,直接和真正負(fù)責(zé)的對(duì)象打交道。
??重構(gòu)壞味道的代碼
? 拆分與移動(dòng)
提煉函數(shù)
我們應(yīng)該將代碼邏輯根據(jù)相關(guān)性拆分為多個(gè)部分,使得每個(gè)部分盡可能的獨(dú)立:
提煉變量
例如下面這一長串的計(jì)算表達(dá)式可讀性就十分差勁,維護(hù)者需要花上不少精力來識(shí)別出不同計(jì)算單元對(duì)應(yīng)的究竟是什么:
我們應(yīng)當(dāng)將每個(gè)計(jì)算單元提煉成一個(gè)有意義的變量,對(duì)于可重用的計(jì)算邏輯可以將它提煉成函數(shù):
提煉類
如同數(shù)據(jù)庫第一范式一樣,我們也需要盡量保證類的成員變量不可再分。否則隨著責(zé)任不斷增加,這個(gè)類會(huì)變得過分復(fù)雜。
區(qū)號(hào)和號(hào)碼都應(yīng)該歸屬于電話實(shí)體,我們應(yīng)當(dāng)提煉出一個(gè)新的類:
拆分循環(huán)
將多件事情放在一個(gè)循環(huán)當(dāng)中處理的確有性能上的優(yōu)勢,但是對(duì)于保證代碼的可維護(hù)性和可測試性不太有利。后文我們也會(huì)提到單一職責(zé)原則是提高內(nèi)聚性,降低耦合性的首要原則:
將處理邏輯拆分開, 更方便我們的封裝與組合:
搬移字段
發(fā)現(xiàn)我們發(fā)現(xiàn)每當(dāng)調(diào)用某個(gè)函數(shù)時(shí),除了傳入一個(gè)記錄參數(shù),還總是需要同時(shí)傳入另一條記錄的某個(gè)字段一起作為參數(shù)時(shí),那就說明很可能有字段放錯(cuò)了位置, 例如:
我們應(yīng)該將 discountRate 屬性移動(dòng)到 plan 實(shí)體上:
? 封裝與組合
封裝變量
對(duì)于所有可變的數(shù)據(jù),只要它的作用域超出單個(gè)函數(shù),我們就應(yīng)該將其封裝起來,只允許通過函數(shù)訪問。數(shù)據(jù)的作用域越大,封裝就越重要。例如:
只暴露 set 和 get 方法,使得修改和查詢的來源變得更為清晰:
或者我們可以使用類的形式來暴露可變數(shù)據(jù):
另外對(duì)于集合類型的變量我們應(yīng)該注意: 不要只對(duì)集合變量的訪問進(jìn)行了封裝,但依然讓取值函數(shù)返回集合本身。這使得集合的成員變量可以直接被修改,而封裝它的類則全然不知。
將增刪操作封裝為成員方法:
引入?yún)?shù)對(duì)象
引入?yún)?shù)對(duì)象不僅可以減少數(shù)據(jù)泥團(tuán),對(duì)于過長的參數(shù)列表也能夠提高可讀性。以筆者自身經(jīng)驗(yàn)而言,當(dāng)一個(gè)函數(shù)存在三個(gè)以上的參數(shù)時(shí),參數(shù)對(duì)象是必不可少的。
將數(shù)據(jù)泥團(tuán)組合成參數(shù)對(duì)象:
函數(shù)組合成類
如果發(fā)現(xiàn)一組函數(shù)形影不離地操作同一塊數(shù)據(jù)(通常是將這塊數(shù)據(jù)作為參數(shù)傳遞給函數(shù)),就是時(shí)候組建一個(gè)類了。類能明確地給這些函數(shù)提供一個(gè)共用的環(huán)境,在對(duì)象內(nèi)部調(diào)用這些函數(shù)可以少傳許多參數(shù),從而簡化函數(shù)調(diào)用,并且這樣一個(gè)對(duì)象也可以更方便地傳遞給系統(tǒng)的其他部分。
將一組函數(shù)組合成類:
查詢?nèi)〈R時(shí)變量
臨時(shí)變量的一個(gè)作用是保存某段代碼的返回值,以便在函數(shù)的后面部分使用它。對(duì)于一個(gè)類而言,在其他成員方法中使用同一個(gè)臨時(shí)變量的概率是很大的,大多數(shù)時(shí)候我們可以將臨時(shí)變量提煉為函數(shù)。
將可以被復(fù)用的部分提煉為查詢函數(shù):
查詢?nèi)〈缮兞?/h1>
計(jì)算往往能更清晰地表達(dá)數(shù)據(jù)的含義,而且也避免了“源數(shù)據(jù)修改時(shí)忘了更新派生變量”的錯(cuò)誤。
用計(jì)算屬性取代派生變量, ?這樣的寫法對(duì)于使用過 vue.js 的同學(xué)應(yīng)該十分熟悉:
? 簡化條件邏輯
替換算法
為完成一個(gè)功能每個(gè)程序員都會(huì)有不同的算法或者說寫法,通常來說越清晰的算法可維護(hù)性越高。當(dāng)發(fā)現(xiàn)做一件事可以有更清晰的方式,你就就應(yīng)該用比較清晰的方式取代復(fù)雜的方式。
我們其實(shí)并不需要這些條件語句:
拆分條件表達(dá)式
這條其實(shí)是提煉函數(shù)的一個(gè)特例,但是由于強(qiáng)調(diào)這一點(diǎn)有很大的價(jià)值,因此單獨(dú)拆出來講:
對(duì)于復(fù)雜的條件語句我們應(yīng)當(dāng)將條件判斷與條件分支抽成三個(gè)函數(shù):
合并條件表達(dá)式
當(dāng)檢查條件各不相同,最終行為卻一致時(shí),應(yīng)該使用“邏輯或”和“邏輯與”將它們合并為一個(gè)條件表達(dá)式。從而使條件檢查的用意更清晰:
將返回值一致的條件合并:
衛(wèi)語句取代嵌套條件表達(dá)式
如果某個(gè)條件極其罕見,就應(yīng)該單獨(dú)檢查該條件,并在該條件為真時(shí)立刻從函數(shù)中返回。這樣的單獨(dú)檢查常常被稱為“衛(wèi)語句”(guard clauses)。
使用衛(wèi)語句可以大大減少嵌套語句的數(shù)量,增強(qiáng)可讀性:
多態(tài)取代條件表達(dá)式
典型的場景是存在好幾個(gè)函數(shù)都有基于類型代碼的 switch 語句。我們可以針對(duì) switch 語句中的每種分支邏輯創(chuàng)建一個(gè)類,用多態(tài)來承載各個(gè)類型特有的行為,從而去除重復(fù)的分支邏輯。
利用多態(tài)代替 switch 語句:
??總結(jié)

必需
不要相信任何人: ?包括用戶的輸入和后端的返回值
對(duì)于視圖中使用到的變量, ?必須保證取值不會(huì)導(dǎo)致異常
對(duì)于核心函數(shù)進(jìn)行單元測試, 必須包括邊界測試和反向測試
使用有意義且統(tǒng)一風(fēng)格的命名: 名副其實(shí),避免誤導(dǎo)
避免出現(xiàn)重復(fù)代碼
避免出現(xiàn) Dead Code
避免過長的參數(shù)列表/函數(shù)/類
建議
符合單一職責(zé)原則
符合開放-封閉原則
避免內(nèi)幕交易
避免依戀情結(jié)
避免夸夸其談通用性
對(duì)于 export 出的對(duì)象字面量使用封裝變量
??推薦閱讀
《代碼整潔之道》?https://book.douban.com/subject/4199741/
《重構(gòu)(第 2 版)》https://book.douban.com/subject/30468597/
《編寫可讀代碼的藝術(shù)》 https://book.douban.com/subject/10797189/
《React 性能優(yōu)化 | 包括原理、技巧、Demo、工具使用》 https://juejin.cn/post/6935584878071119885
《敏捷軟件開發(fā) : 原則、模式與實(shí)踐》 https://book.douban.com/subject/1140457/
《阿里巴巴 Java 開發(fā)手冊(cè)》 https://kangroo.gitee.io/ajcg/#/naming-style
《naming-cheatsheet》 https://github.com/kettanaito/naming-cheatsheet