快哭了!我被同事寫的代碼坑慘了
寫出整潔的代碼,是每個程序員的追求。《clean code》指出,要想寫出好的代碼,首先得知道什么是骯臟代碼、什么是整潔代碼;然后通過大量的刻意練習(xí),才能真正寫出整潔的代碼。
圖片來自 Pexels
WTF/min 是衡量代碼質(zhì)量的唯一標準,Uncle Bob 在書中稱糟糕的代碼為沼澤(wading),這只突出了我們是糟糕代碼的受害者。
國內(nèi)有一個更適合的詞匯:屎山,雖然不是很文雅但是更加客觀,程序員既是受害者也是加害者。
對于什么是整潔的代碼,書中給出了大師們的總結(jié):
- Bjarne Stroustrup:優(yōu)雅且高效;直截了當;減少依賴;只做好一件事
- Grady booch:簡單直接
- Dave thomas:可讀,可維護,單元測試
- Ron Jeffries:不要重復(fù)、單一職責(zé),表達力(Expressiveness)
其中,我最喜歡的是表達力(Expressiveness)這個描述,這個詞似乎道出了好代碼的真諦:用簡單直接的方式描繪出代碼的功能,不多也不少。
命名的藝術(shù)
坦白的說,命名是一件困難的事情,要想出一個恰到好處的命名需要一番功夫,尤其我們的母語還不是編程語言所通用的英語。
不過這一切都是值得了,好的命名讓你的代碼更直觀,更有表達力。好的命名應(yīng)該有下面的特征:
①名副其實
好的變量名告訴你:是什么東西,為什么存在,該怎么使用,如果需要通過注釋來解釋變量,那么就先得不那么名副其實了。
下面是書中的一個示例代碼,展示了命名對代碼質(zhì)量的提升:
- # bad code
- def getItem(theList):
- ret = []
- for x in theList:
- if x[0] == 4:
- ret.append(x)
- return ret
- # good code
- def getFlaggedCell(gameBoard):
- '''掃雷游戲,flagged: 翻轉(zhuǎn)'''
- flaggedCells = []
- for cell in gameBoard:
- if cell.IsFlagged():
- flaggedCells.append(cell)
- return flaggedCells
②避免誤導(dǎo)
不要掛羊頭賣狗肉,不要覆蓋慣用縮略語!
這里不得不吐槽前兩天才看到的一份代碼,居然使用了 l 作為變量名;而且,user 居然是一個 list(單復(fù)數(shù)都沒學(xué)好!!)
③有意義的區(qū)分
代碼是寫給機器執(zhí)行,也是給人閱讀的,所以概念一定要有區(qū)分度:
- # bad
- def copy(a_list, b_list):
- pass
- # good
- def copy(source, destination):
- pass
④使用讀的出來的單詞
如果名稱讀不出來,那么討論的時候就會像個傻鳥。
⑤使用方便搜索的命名
名字長短應(yīng)與其作用域大小相對應(yīng)!
⑥避免思維映射
比如在代碼中寫一個 temp,那么讀者就得每次看到這個單詞的時候翻譯成其真正的意義。
注釋
有表達力的代碼是無需注釋的:
- The proper use of comments is to compensate for our failure to express ourself in code.
注釋的適當作用在于彌補我們用代碼表達意圖時遇到的失敗,這聽起來讓人沮喪,但事實確實如此。
The truth is in the code,注釋只是二手信息,二者的不同步或者不等價是注釋的最大問題。
書中給出了一個非常形象的例子來展示,用代碼來闡述,而非注釋:
- bad
- // check to see if the employee is eligible for full benefit
- if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))
- good
- if (employee.isEligibleForFullBenefits())
因此,當想要添加注釋的時候,可以想想是否可以通過修改命名,或者修改函數(shù)(代碼)的抽象層級來展示代碼的意圖。
當然,也不能因噎廢食,書中指出了以下一些情況屬于好的注釋:
- 法務(wù)信息
- 對意圖的注釋,為什么要這么做
- 警示
- TODO 注釋
- 放大看似不合理之物的重要性
其中個人最贊同的是第 2 點和第 5 點,做什么很容易通過命名表達,但為什么要這么做則并不直觀,特別涉及到專業(yè)知識、算法的時候。
另外,有些第一感覺“不那么優(yōu)雅”的代碼,也許有其特殊愿意,那么這樣的代碼就應(yīng)該加上注釋,說明為什么要這樣,比如為了提升關(guān)鍵路徑的性能,可能會犧牲部分代碼的可讀性。
最壞的注釋就是過時或者錯誤的注釋,這對于代碼的維護者(也許就是幾個月后的自己)是巨大的傷害,可惜除了 code review,并沒有簡單易行的方法來保證代碼與注釋的同步。
函數(shù)
①函數(shù)的單一職責(zé)
一個函數(shù)應(yīng)該只做一件事,這件事應(yīng)該能通過函數(shù)名就能清晰的展示。判斷方法很簡單:看看函數(shù)是否還能再拆出一個函數(shù)。
函數(shù)要么做什么 do_sth,要么查詢什么 query_sth。最惡心的就是函數(shù)名表示只會 query_sth,但事實上卻會 do_sth,這使得函數(shù)產(chǎn)生了副作用。
比如書中的例子:
- public class UserValidator {
- private Cryptographer cryptographer;
- public boolean checkPassword(String userName, String password) {
- User user = UserGateway.findByName(userName);
- if (user != User.NULL) {
- String codedPhrase = user.getPhraseEncodedByPassword();
- String phrase = cryptographer.decrypt(codedPhrase, password);
- if ("Valid Password".equals(phrase)) {
- Session.initialize();
- return true;
- }
- }
- return false;
- }
- }
②函數(shù)的抽象層級
每個函數(shù)一個抽象層次,函數(shù)中的語句都要在同一個抽象層級,不同的抽象層級不能放在一起。
比如我們想把大象放進冰箱,應(yīng)該是這個樣子的:
- def pushElephantIntoRefrige():
- openRefrige()
- pushElephant()
- closeRefrige()
函數(shù)里面的三句代碼在同一個層級(高度)描述了要完成把大象放進冰箱這件事順序相關(guān)的三個步驟。
顯然,pushElephant 這個步驟又可能包含很多子步驟,但是在 pushElephantIntoRefrige 這個層級,是無需知道太多細節(jié)的。
當我們想通過閱讀代碼的方式來了解一個新的項目時,一般都是采取廣度優(yōu)先的策略,自上而下的閱讀代碼,先了解整體結(jié)構(gòu),然后再深入感興趣的細節(jié)。
如果沒有對實現(xiàn)細節(jié)進行良好的抽象(并凝練出一個名副其實的函數(shù)),那么閱讀者就容易迷失在細節(jié)的汪洋里。
某種程度看來,這個跟金字塔原理也很像:
每一個層級都是為了論證其上一層級的觀點,同時也需要下一層級的支持;同一層級之間的多個論點又需要以某種邏輯關(guān)系排序。
pushElephantIntoRefrige 就是中心論點,需要多個子步驟的支持,同時這些子步驟之間也有邏輯先后順序。
③函數(shù)參數(shù)
函數(shù)的參數(shù)越多,組合出的輸入情況就愈多,需要的測試用例也就越多,也就越容易出問題。
輸出參數(shù)相比返回值難以理解,這點深有同感,輸出參數(shù)實在是很不直觀。從函數(shù)調(diào)用者的角度,一眼就能看出返回值,而很難識別輸出參數(shù)。輸出參數(shù)通常逼迫調(diào)用者去檢查函數(shù)簽名,這個實在不友好。
向函數(shù)傳入Boolean(書中稱之為 Flag Argument)通常不是好主意。尤其是傳入True or False后的行為并不是一件事情的兩面,而是兩件不同的事情時。
這很明顯違背了函數(shù)的單一職責(zé)約束,解決辦法很簡單,那就是用兩個函數(shù)。Dont repear yourself。
在函數(shù)這個層級,是最容易、最直觀實現(xiàn)復(fù)用的,很多 IDE 也難幫助我們講一段代碼重構(gòu)出一個函數(shù)。
不過在實踐中,也會出現(xiàn)這樣一種情況:一段代碼在多個方法中都有使用,但是又不完全一樣,如果抽象成一個通用函數(shù),那么就需要加參數(shù)、加 if else 區(qū)別。這樣就有點尷尬,貌似可以重構(gòu),但又不是很完美。
造成上述問題的某種情況是因為,這段代碼也違背了單一職責(zé)原則,做了不只一件事情,這才導(dǎo)致不好復(fù)用,解決辦法是進行方法的細分,才能更好復(fù)用。
也可以考慮 template method 來處理差異的部分。
測試
非常慚愧的是,在我經(jīng)歷的項目中,測試(尤其是單元測試)一直都沒有得到足夠的重視,也沒有試行過 TDD。正因為缺失,才更感良好測試的珍貴。
我們常說,好的代碼需要有可讀性、可維護性、可擴展性,好的代碼、架構(gòu)需要不停的重構(gòu)、迭代,但自動化測試是保證這一切的基礎(chǔ),沒有高覆蓋率的、自動化的單元測試、回歸測試,誰都不敢去修改代碼,只能任其腐爛。
即使針對核心模塊寫了單元測試,一般也很隨意,認為這只是測試代碼,配不上生產(chǎn)代碼的地位,以為只要能跑通就行了。
這就導(dǎo)致測試代碼的可讀性、可維護性非常差,然后導(dǎo)致測試代碼很難跟隨生產(chǎn)代碼一起更新、演化,最后導(dǎo)致測試代碼失效。所以說,臟測試等同于沒測試。
因此,測試代碼的三要素:
- 可讀性
- 可讀性
- 可讀性
對于測試的原則、準則如下:
- 沒有測試之前不要寫任何功能代碼
- 只編寫恰好能夠體現(xiàn)一個失敗情況的測試代碼
- 只編寫恰好能通過測試的功能代碼
測試的 FIRST 準則:
- 快速(Fast)測試應(yīng)該夠快,盡量自動化。
- 獨立(Independent)測試應(yīng)該應(yīng)該獨立。不要相互依賴
- 可重復(fù)(Repeatable)測試應(yīng)該在任何環(huán)境上都能重復(fù)通過。
- 自我驗證(Self-Validating)測試應(yīng)該有 bool 輸出。不要通過查看日志這種低效率方式來判斷測試是否通過。
- 及時(Timely)測試應(yīng)該及時編寫,在其對應(yīng)的生產(chǎn)代碼之前編寫。