程式碼整潔 vs 程式碼骯髒
寫出整潔的程式碼,是每個程式設計師的追求。《clean code》指出,要想寫出好的程式碼,首先得知道什麼是骯髒程式碼、什麼是整潔程式碼;然後通過大量的刻意練習,才能真正寫出整潔的程式碼。
WTF/min是衡量程式碼質量的唯一標準,Uncle Bob在書中稱糟糕的程式碼為沼澤(wading),這隻突出了我們是糟糕程式碼的受害者。國內有一個更適合的詞彙:屎山,雖然不是很文雅但是更加客觀,程式設計師既是受害者也是加害者。
對於什麼是整潔的程式碼,書中給出了大師們的總結:
-
Bjarne Stroustrup:優雅且高效;直截了當;減少依賴;只做好一件事
-
Grady booch:簡單直接
-
Dave thomas:可讀,可維護,單元測試
-
Ron Jeffries:不要重複、單一職責,表達力(Expressiveness)
其中,我最喜歡的是表達力(Expressiveness)這個描述,這個詞似乎道出了好程式碼的真諦:用簡單直接的方式描繪出程式碼的功能,不多也不少。
本文記錄閱讀《clean code》之後個人“深有同感”或者“醍醐灌頂”的一些觀點。
一、命名的藝術
坦白的說,命名是一件困難的事情,要想出一個恰到好處的命名需要一番功夫,尤其我們的母語還不是程式語言所通用的英語。不過這一切都是值得了,好的命名讓你的程式碼更直觀,更有表達力。
好的命名應該有下面的特徵:
1.1 名副其實
好的變數名告訴你:是什麼東西,為什麼存在,該怎麼使用
如果需要通過註釋來解釋變數,那麼就先得不那麼名副其實了。
下面是書中的一個示例程式碼,展示了命名對程式碼質量的提升
# 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: 翻轉'
''
flaggedCells
=
[
]
for cell
in gameBoard
:
if cell
.
IsFlagged
(
)
:
flaggedCells
.
append
(cell
)
return flaggedCells
1.2 避免誤導
-
不要掛羊頭賣狗肉
-
不要覆蓋慣用縮略語
這裡不得不吐槽前兩天才看到的一份程式碼,居然使用了 l 作為變數名;而且,user居然是一個list(單複數都沒學好!!)
1.3 有意義的區分
程式碼是寫給機器執行,也是給人閱讀的,所以概念一定要有區分度。
# bad
def
copy
(a_list
, b_list
)
:
pass
# good
def
copy
(source
, destination
)
:
pass
1.4 使用讀的出來的單詞
如果名稱讀不出來,那麼討論的時候就會像個傻鳥
1.5 使用方便搜尋的命名
名字長短應與其作用域大小相對應
1.6 避免思維對映
比如在程式碼中寫一個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
)
)
//vx耗:mbz_java_panlong 十年開發經驗程式設計師,免費解答,備註“MI”即可
good
if
(employee
.
isEligibleForFullBenefits
(
)
)
因此,當想要新增註釋的時候,可以想想是否可以通過修改命名,或者修改函式(程式碼)的抽象層級來展示程式碼的意圖。
當然,也不能因噎廢食,書中指出了以下一些情況屬於好的註釋
-
法務資訊
-
對意圖的註釋,為什麼要這麼做
-
警示
-
TODO註釋
-
放大看似不合理之物的重要性
其中個人最贊同的是第2點和第5點,做什麼很容易通過命名錶達,但為什麼要這麼做則並不直觀,特別涉及到專業知識、演算法的時候。另外,有些第一感覺“不那麼優雅”的程式碼,也許有其特殊願意,那麼這樣的程式碼就應該加上註釋,說明為什麼要這樣,比如為了提升關鍵路徑的效能,可能會犧牲部分程式碼的可讀性。
最壞的註釋就是過時或者錯誤的註釋,這對於程式碼的維護者(也許就是幾個月後的自己)是巨大的傷害,可惜除了code review,並沒有簡單易行的方法來保證程式碼與註釋的同步。
三、函式
3.1 函式的單一職責
一個函式應該只做一件事,這件事應該能通過函式名就能清晰的展示。判斷方法很簡單:看看函式是否還能再拆出一個函式。
函式要麼做什麼do_sth, 要麼查詢什麼query_sth。最噁心的就是函式名錶示只會query_sth, 但事實上卻會do_sth, 這使得函式產生了副作用。比如書中的例子
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
;
}
}
3.2 函式的抽象層級
每個函式一個抽象層次,函式中的語句都要在同一個抽象層級,不同的抽象層級不能放在一起。比如我們想把大象放進冰箱,應該是這個樣子的:
def
pushElephantIntoRefrige
(
)
:
openRefrige
(
)
pushElephant
(
)
closeRefrige
(
)
函式裡面的三句程式碼在同一個層級(高度)描述了要完成把大象放進冰箱這件事順序相關的三個步驟。顯然,pushElephant這個步驟又可能包含很多子步驟,但是在pushElephantIntoRefrige這個層級,是無需知道太多細節的。
當我們想通過閱讀程式碼的方式來了解一個新的專案時,一般都是採取廣度優先的策略,自上而下的閱讀程式碼,先了解整體結構,然後再深入感興趣的細節。如果沒有對實現細節進行良好的抽象(並凝練出一個名副其實的函式),那麼閱讀者就容易迷失在細節的汪洋裡。
某種程度看來,這個跟金字塔原理也很像
每一個層級都是為了論證其上一層級的觀點,同時也需要下一層級的支援;同一層級之間的多個論點又需要以某種邏輯關係排序。pushElephantIntoRefrige就是中心論點,需要多個子步驟的支援,同時這些子步驟之間也有邏輯先後順序。
3.3 函式引數
函式的引數越多,組合出的輸入情況就愈多,需要的測試用例也就越多,也就越容易出問題。
輸出引數相比返回值難以理解,這點深有同感,輸出引數實在是很不直觀。從函式呼叫者的角度,一眼就能看出返回值,而很難識別輸出引數。輸出引數通常逼迫呼叫者去檢查函式簽名,這個實在不友好。
向函式傳入Boolean(書中稱之為 Flag Argument)通常不是好主意。尤其是傳入True or False後的行為並不是一件事情的兩面,而是兩件不同的事情時。這很明顯違背了函式的單一職責約束,解決辦法很簡單,那就是用兩個函式。
3.4 Dont repear yourself
在函式這個層級,是最容易、最直觀實現複用的,很多IDE也難幫助我們講一段程式碼重構出一個函式。
不過在實踐中,也會出現這樣一種情況:一段程式碼在多個方法中都有使用,但是又不完全一樣,如果抽象成一個通用函式,那麼就需要加引數、加if else區別。這樣就有點尷尬,貌似可以重構,但又不是很完美。
造成上述問題的某種情況是因為,這段程式碼也違背了單一職責原則,做了不只一件事情,這才導致不好複用,解決辦法是進行方法的細分,才能更好複用。也可以考慮template method來處理差異的部分。
四、測試
非常慚愧的是,在我經歷的專案中,測試(尤其是單元測試)一直都沒有得到足夠的重視,也沒有試行過TDD。正因為缺失,才更感良好測試的珍貴。
我們常說,好的程式碼需要有可讀性、可維護性、可擴充套件性,好的程式碼、架構需要不停的重構、迭代,但自動化測試是保證這一切的基礎,沒有高覆蓋率的、自動化的單元測試、迴歸測試,誰都不敢去修改程式碼,只能任其腐爛。
即使針對核心模組寫了單元測試,一般也很隨意,認為這只是測試程式碼,配不上生產程式碼的地位,以為只要能跑通就行了。這就導致測試程式碼的可讀性、可維護性非常差,然後導致測試程式碼很難跟隨生產程式碼一起更新、演化,最後導致測試程式碼失效。所以說,髒測試 - 等同於 - 沒測試。//vx耗:mbz_java_panlong 十年開發經驗程式設計師,免費解答,備註“MI”即可
因此,測試程式碼的三要素:可讀性,可讀性,可讀性。
對於測試的原則、準則如下:
-
You are not allowed to write any production code unless it is to make a failing unit test pass. 沒有測試之前不要寫任何功能程式碼
-
You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures. 只編寫恰好能夠體現一個失敗情況的測試程式碼
-
You are not allowed to write any more production code than is sufficient to pass the one failing unit test. 只編寫恰好能通過測試的功能程式碼
測試的FIRST準則:
-
快速(Fast)測試應該夠快,儘量自動化。
-
獨立(Independent) 測試應該應該獨立。不要相互依賴
-
可重複(Repeatable) 測試應該在任何環境上都能重複通過。
-
自我驗證(Self-Validating) 測試應該有bool輸出。不要通過檢視日誌這種低效率方式來判斷測試是否通過
-
及時(Timely) 測試應該及時編寫,在其對應的生產程式碼之前編寫
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70010294/viewspace-2848909/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 整潔的程式碼VS卓越的程式碼
- 程式碼整潔之道
- JavaScript 程式碼整潔之道JavaScript
- Typescript 程式碼整潔之道TypeScript
- 聊聊程式碼整潔之道
- 整潔的 Table View 程式碼View
- 程式碼整潔之道 clean code
- 重構 - 程式碼整潔之道
- (譯)保持你的程式碼整潔
- 程式碼整潔之道讀書記
- 如何寫出整潔的程式碼
- React 整潔程式碼最佳實踐React
- 整潔程式碼的4個提示
- 《程式碼整潔之道》精華速覽,助你提升程式碼質量
- 閱讀《程式碼整潔之道》總結
- 程式碼整潔之道 – 有意義的命名
- 程式碼整潔之道之做減法
- 程式碼整潔之道的 7 個方法
- 程式碼整潔之道Clean Code筆記筆記
- 如何讓你的程式碼整潔漂亮
- 谷歌大牛:程式設計的骯髒小祕密谷歌程式設計
- 優秀程式設計師眼中的整潔程式碼程式設計師
- Python程式碼整潔之道--使用裝飾器改進程式碼Python
- 《程式碼整潔之道》總結和筆記筆記
- 讀書筆記-程式碼整潔之道(一)筆記
- 什麼是整潔的程式碼(Clean Code)?
- 程式碼整潔之道--讀書筆記(14)筆記
- 程式碼整潔之道--讀書筆記(1)筆記
- 程式碼整潔之道--讀書筆記(2)筆記
- 程式碼整潔之道--讀書筆記(3)筆記
- 程式碼整潔之道--讀書筆記(4)筆記
- 程式碼整潔之道--讀書筆記(5)筆記
- 程式碼整潔之道--讀書筆記(6)筆記
- 程式碼整潔之道--讀書筆記(7)筆記
- 程式碼整潔之道--讀書筆記(9)筆記
- 程式碼整潔之道--讀書筆記(10)筆記
- 程式碼整潔之道--讀書筆記(11)筆記
- 程式碼整潔之道--讀書筆記(12)筆記