代碼的藝術(shù)-Writing Code Like a Pianist
前言
如何評定一個系統(tǒng)的質(zhì)量?什么樣的系統(tǒng)或者軟件可以稱之為高質(zhì)量?可以從三個角度來看,一是架構(gòu)設(shè)計,例如技術(shù)選型、分布式系統(tǒng)中的數(shù)據(jù)一致性考慮等,二是項目管理,無論是敏捷開發(fā)還是瀑布式開發(fā),都應(yīng)當(dāng)對技術(shù)負(fù)債進(jìn)行清理,對代碼進(jìn)行重構(gòu)等,最后離不開的是代碼質(zhì)量,代碼質(zhì)量的高低直接影響系統(tǒng)的可維護(hù)性和可擴(kuò)展性。好比一輛汽車,內(nèi)飾高級,外觀漂亮,但是底盤不行,動力孱弱,也難以稱得上是一輛好車。本文將從主觀和客觀的角度,和大家探討一下,作為程序員,應(yīng)該如何寫出整潔高質(zhì)量的代碼。
主觀角度
工程師精神
點(diǎn)開京ME頭像可以看到,咱們內(nèi)部的title是“xx開發(fā)工程師”,而不是“xx代碼編寫員”,這個師可以理解為大師的師(master),大師級程序員把系統(tǒng)當(dāng)作故事來講,而不是當(dāng)作程序來寫。作為開發(fā)者,應(yīng)該對自己寫下的代碼負(fù)責(zé),當(dāng)在一個類上@author冠名的時候,應(yīng)當(dāng)有一種成就感,在未來的某一天,可能一年,兩年甚至五年之后,其他同事讀到這段代碼,會由衷的發(fā)出感嘆“牛B”,而不是吐槽“寫的什么玩意”,Doug Lea寫的并發(fā)包,時隔多年,他的大名依然如雷貫耳。
提高代碼可讀性
首先應(yīng)當(dāng)達(dá)成共識的是,代碼是寫給人看的,給機(jī)器運(yùn)行那只是基礎(chǔ),優(yōu)秀的代碼,不需要過多的注釋,代碼本身就是注釋。可讀性指的是,其他開發(fā)人員能夠迅速理解代碼的意圖和功能,簡潔的代碼更易于理解、測試和維護(hù)。其次,避免重復(fù),代碼重復(fù)是軟件中一切邪惡的根源,許多原則和設(shè)計模式都是為了消除重復(fù)而創(chuàng)建。
客觀角度
接下來從客觀角度,例舉日常代碼評審中的常見問題,給大家作一些參考。
try/catch使用
1. try/catch塊應(yīng)該盡量的小,只針對無法預(yù)料的情況進(jìn)行try/catch,可以預(yù)見的異常情況應(yīng)該在try之前被校驗住。
無法預(yù)料的情況:遠(yuǎn)程調(diào)用,IO讀寫,第三方API等。 可以預(yù)見的情況:某個參數(shù)為空,某個列表無元素等。

try/catch塊小有什么好處?
2. 如果確實(shí)需要對整段方法進(jìn)行tryCatch,不如新寫一個方法,將業(yè)務(wù)邏輯處理和異常處理區(qū)分開來。
例如:

try/catch代碼塊中的內(nèi)容多,說明對代碼掌控能力不夠好 如果存在try,那么盡量保證try在第一行,并且在catch/finally后不該有其他內(nèi)容
方法長度
方法行數(shù)應(yīng)該盡可能的短,越短越好。能夠讓讀者點(diǎn)進(jìn)來的一瞬間就明白這個方法做了什么事情。舉個例子:

這是一個查詢價格的方法,這個方法長度截圖已經(jīng)一頁截不下了。如果要想搞明白這個方法做了什么事,憑借方法體中的注釋是沒法輕易梳理清楚。
只需要做簡單的一些處理的包裝,這個方法就能變得通俗易懂,而且并不用寫一行注釋。
簡單重構(gòu)后:



從上面的代碼上看,第一段將價格查詢的代碼分成了兩個部分,首先解析查詢的審批狀態(tài),然后根據(jù)審批狀態(tài)分別查詢價格。第二段將價格查詢分成了兩個部分,一個是查詢審批中的價格,另一個是查詢審批通過的價格,最后將二者組合起來返回。第三段是截了審批中價格查詢的代碼,顯而易見地,查詢審批中價格有兩種模式,百分比模式和底價模式,查詢到結(jié)果后,組合返回即得到商品的渠道價。
對比一下不難看出,原代碼塊篇幅過長,嵌套較深(for循環(huán)嵌套if再嵌套if),導(dǎo)致可讀性下降。如果我想改動某塊的代碼,都需要深思熟慮。但是在拆分之后,就可以快速定位到目標(biāo)代碼塊,并且不用擔(dān)心對其他方法產(chǎn)生影響,另外,對其進(jìn)行單元測試也會比較方便編寫測試用例。
禁止在方法內(nèi)部修改入?yún)?shù)據(jù)
從系統(tǒng)概念上來看,一般可以將系統(tǒng)分為兩類,一類是給一個輸入,得到一個輸出,這類稱之為響應(yīng)式,另一類是給一個輸入,但是沒有輸出,這類被認(rèn)為是命令式。同理,對于方法而言,響應(yīng)式方法即給定一個入?yún)?,得到一個出參,比較常見的比如模型轉(zhuǎn)換、數(shù)據(jù)查詢等,命令式方法有發(fā)送消息、run一個線程。
但是無論怎么變化,這些都有一個共同點(diǎn),他們不會去修改入?yún)⒌臄?shù)據(jù)。
舉個例子:

這里調(diào)用了3個set方法,但是誰能一眼看出來,它把值set到哪個object里了?還得挨個點(diǎn)進(jìn)去看看具體實(shí)現(xiàn),才知道在哪里賦值了。
如果確實(shí)需要修改參數(shù)的值,可以創(chuàng)建一個新的對象來存儲修改后的結(jié)果,并將其返回給調(diào)用者。這樣可以保持代碼的清晰性、可維護(hù)性和可測試性,減少意外的副作用和數(shù)據(jù)一致性問題。
方法參數(shù)
方法的參數(shù)也是越少越好,最理想的參數(shù)是無參,其次是單參,應(yīng)該盡量避免三個或者三個以上的參數(shù)
繼續(xù)上面的代碼,最后一行“設(shè)置渠道類型”,set方法里有兩個入?yún)?,完全可以將其賦值對象提取出來,減少一個入?yún)?,如下?/p>

如果方法的處理邏輯,只依賴于某一局部變量object的數(shù)據(jù),那么完全可以將這個方法放在對象內(nèi)部,進(jìn)一步減少方法參數(shù),提高代碼復(fù)用性,例如:

這樣來看,就成功將兩個入?yún)⒌姆椒?,縮減成了無參方法,和原始代碼相比,原始代碼方法的作用域僅限于類內(nèi)部,而最終的無參方法,只需要拿到目標(biāo)對象實(shí)例即可在任何地方調(diào)用,還能減少重復(fù)代碼的編寫。
減少縮進(jìn)
代碼中出現(xiàn)大量的縮進(jìn)顯然是影響閱讀的,在可能的情況下,我們應(yīng)該盡可能的減少代碼中的縮進(jìn)和層次。好的代碼讀起來應(yīng)該是和報紙一樣,排版整齊優(yōu)雅,而不是爬樓梯。
來看看下面的代碼:

代碼中出現(xiàn)兩層if嵌套,首先我們可以對第一層if嵌套做一個簡化,簡單將if的判斷條件做一個反轉(zhuǎn)得到下面代碼:

這樣就減少了一層縮進(jìn),然后再將變量和參數(shù)進(jìn)行一些微調(diào):

這樣,和之前的代碼比起來,是不是可讀性提高了?
單一職責(zé)
在面向?qū)ο缶幊讨校?單一職責(zé)原則"(Single Responsibility Principle,SRP)是指一個類或模塊應(yīng)該只有一個引起它變化的原因,即一個類或模塊應(yīng)該只有一個主要責(zé)任或目標(biāo)。通過細(xì)化功能和職責(zé),可以提高代碼的可維護(hù)性、復(fù)用性和可擴(kuò)展性。
看下面的代碼:

這個方法是從聲明上來看,是用于保存任務(wù)數(shù)據(jù),但是實(shí)際上如果入?yún)⒅械哪承?shù)據(jù)不為空(實(shí)際上看這個方法的實(shí)現(xiàn)也很難一眼看出來具體是哪些數(shù)據(jù)),會進(jìn)行更新update操作。這種寫法不僅容易讓讀者感到困惑,也會降低代碼的可擴(kuò)展性。
Q:AtomicInteger的compareAndSet方法違反了單一職責(zé)嗎? A:并不違反單一職責(zé)原則。compareAndSet 方法是 AtomicInteger 類提供的一種原子操作,用于比較當(dāng)前值與給定期望值是否相等,如果相等則將當(dāng)前值設(shè)置為新值。這個方法的職責(zé)是實(shí)現(xiàn)原子性的比較和設(shè)置操作,確保在多線程環(huán)境下的線程安全性。compareAndSet 方法作為其中的一種操作,是為了滿足特定的需求而設(shè)計的,它并不違反單一職責(zé)原則。單一職責(zé)原則要求一個類或模塊只有一個主要責(zé)任,而 AtomicInteger 類的主要責(zé)任是提供原子整數(shù)操作,compareAndSet 方法是其中的一部分,屬于該類主要責(zé)任的一部分。因此,compareAndSet 方法并不違反單一職責(zé)原則。
使用意義明確的命名
下面的代碼,checkParam的返回值是布爾值,但是作為讀者,我不知道是應(yīng)該check通過了返回True,還是check失敗的情況下返回True。

一般而言,check作為開頭的方法名稱,沒有返回值,對于check不通過的情況以異常的形式拋出。返回布爾值的方法通常以is作為起始,換種寫法看看是不是會好讀很多?

使用有意義的命名
這里比較典型的是flow編排文件里的條件判斷節(jié)點(diǎn),使用0,1這類沒有意義的數(shù)字,很難一眼看出來說了什么,需要挨個點(diǎn)進(jìn)去看邏輯,才能梳理明白,改成有意義的命名,便于理解。

類、方法命名規(guī)則
類名使用名詞或者名詞短語 方法命名使用動詞或者動名詞結(jié)構(gòu)
這個比較好理解,暫不舉例了。
不要留無關(guān)內(nèi)容
在發(fā)起MR之前,檢查一遍代碼里有沒有“被注釋掉的代碼”以及TODO。
對于注釋掉的代碼而言,如果后續(xù)有其他開發(fā)人員介入,他們會覺得非常困惑,這兩行代碼為什么要注釋掉?它們重要嗎?它們放在那里,是因為在未來的某一天需要用?還是說只是當(dāng)時忘記刪除了?還是說為了給未來修改做提示?這些顧慮可能讓其他開發(fā)人員也不敢動手清理這些被注釋的代碼,進(jìn)而造成這些代碼被永久的保留下來,形成“幽靈代碼”。
對于TODO來說,建議在寫todo的時候,要求在后面跟上自己的ERP,誰來解決,怎么解決,deadLine是什么時候。因為在大多數(shù)情況下,Later equals never。久而久之,連自己都忘了這個todo,忘了當(dāng)時為啥寫這個todo,應(yīng)該怎么處理,給系統(tǒng)埋下隱患。

如上,當(dāng)年寫下這個TODO的同事已經(jīng)離職了,現(xiàn)在只有上帝才知道todo什么東西。
枚舉
養(yǎng)成良好的習(xí)慣,在用枚舉定義的變量,拉一條引用指向枚舉,方便其他開發(fā)人員閱讀。
例如:

這是一個可窮舉的變量(因為狀態(tài)是有限的),但是讀者不知道具體都有哪些狀態(tài)。這里其實(shí)是有一個枚舉關(guān)聯(lián)上來的.

為了方便閱讀,可以在變量定義的地方,使用javaDoc的@see/@link方式,說明這個變量的枚舉范圍。

單元測試
單測的重要性不言而喻,這里先挖個坑,后續(xù)單開一篇寫。
結(jié)語
寫在最后,給大家推薦一本經(jīng)典書籍《代碼整潔之道》,實(shí)際上該書的出版時間較早,書中的某些知識或許有些過時,但是前面幾章內(nèi)容仍然值得一讀。整潔的代碼需要團(tuán)隊的共同努力,團(tuán)隊成員應(yīng)該遵循一致的編碼風(fēng)格和標(biāo)準(zhǔn),進(jìn)行代碼審查和知識分享,共同維護(hù)和改進(jìn)代碼質(zhì)量。它不僅僅是一種編碼風(fēng)格,更是一種思維方式和價值觀。優(yōu)雅的代碼,就像是藝術(shù)品,正如標(biāo)題所言,應(yīng)當(dāng)像一名鋼琴家一樣編寫代碼。
作者:京東零售 譚磊
來源:京東云開發(fā)者社區(qū) 轉(zhuǎn)載請注明來源
浙公網(wǎng)安備 33010602011771號