讀別人的程式是一件苦差事,不僅需要具備對應的領域知識,也得了解對方的程式書寫邏輯與風格。因此,為了讓程式碼容易被人理解,整潔程式碼是必須持續精進的技藝。
在 code review 進行實質審查之前,我喜歡先做一點點形式審查,尤其是某些 code smell,臭味暴風半徑大到甚至連領域知識的外行人都聞得到。
我很常遇到不必要的巢狀迴圈或條件判斷式。經驗告訴我,除非真的是要設計出什麼高大尚的演算法或者刷題,否則,這有相當大的機率是需要進一步 refactor 的。
像以下這段程式,即使我故意把領域知識相關細節用 conditionXX
馬賽克掉了,你還是可以明顯聞到 bad smell:迴圈 + 條件判斷式,ABCDEF 深達 6 層,你能夠很快捕捉到箇中邏輯嗎?
# condition placeholders
condition1 = condition2 = condition3 = condition4 = condition5 = condition6 = lambda x : True
data = [
# area code -> city name
{ 201: 'Jersey City', 206: 'Seattle' },
{ 212: 'New York', 213: 'Los Angeles' },
{ 226: 'London', 236: 'Vancouver' }
]
def demo1():
for item in data: # A
if condition1(item): # B1
for pair in item.items(): # C
print(pair)
if condition2(pair): # D
if condition3(pair): # E
if condition4(pair): # F1
print('CASE 1')
elif condition5(pair): # F2
print('CASE 2')
else: # B2
print('CASE 3')
再往下看之前,請先自己嘗試 refactor 看看。
頭重腳輕
通常我會先針對頭重腳輕的 if-else 開刀。
頭重腳輕的 if-else 有什麼問題呢?請試著將以下的程式讀到第 4 行,然後稍作暫停:
01: if condition1(item):
02: # ...
03: # ...
04: # many lines of code... (頭重)
05: # ...
06: # ...
07: # ...
...
20: else:
21: # only 1 line of code (腳輕)
此刻,儘管你才讀到第 4 行,但應該會有個懸念:「究竟 if 的另一隻腳 else 會是什麼?」可是你要一直往下讀到第 20 行,謎底才會揭曉。
這種懸念,有個心理學名詞:Zeigarnik effect(常譯為齊加尼克效應、蔡加尼克效應)。
這種懸念,會增加我們的認知負荷,瓜分我們的專注力————閱讀程式碼,是最需要專注力的。
如果換個順序,頭輕腳重,就可以盡早排除懸念,因為懸念早在你讀到第 3 行就已結束:
01: if not condition1(item):
02: # only 1 line of code (頭輕)
03: else:
04: # ...
05: # ...
06: # many lines of code... (腳重)
07: # ...
08: # ...
09: # ...
將這一招套用在本文一開頭的範例程式,可以改寫成:
def demo2():
for item in data: # A
if not condition1(item): # B1 + B2
print('CASE 3')
continue
for pair in item.items(): # C
print(pair)
if condition2(pair): # D
if condition3(pair): # E
if condition4(pair): # F1
print('CASE 1')
elif condition5(pair): # F2
print('CASE 2')
這一招,在 Refactoring 書中叫做 “Replace Nested Conditional with Guard Clauses”。 1
整併
連續的巢狀 if,往往給讀者一種「這裡一定藏著很複雜的邏輯」的感覺————或者錯覺。如有可能,應該嘗試用 and 及 or 加以整併。
像前面的範例程式,如果繼續將 condition2 & condition3 整併,巢狀深度降低了,是不是更易讀呢?
def demo3():
for item in data: # A
if not condition1(item): # B1 + B2
print('CASE 3')
continue
for pair in item.items(): # C
print(pair)
if condition2(pair) and condition3(pair): # D + E
if condition4(pair): # F1
print('CASE 1')
elif condition5(pair): # F2
print('CASE 2')
如果行有餘力,甚至還可以替整併起來的條件判斷式取個有意義的名字(function 或 lambda 或 method),程式碼就更具有自我詮釋的功能:
def demo4():
for item in data: # A
if not condition1(item): # B1 + B2
print('CASE 3')
continue
for pair in item.items(): # C
print(pair)
if condition6(pair): # D + E
if condition4(pair): # F1
print('CASE 1')
elif condition5(pair): # F2
print('CASE 2')
這一招,在 Refactoring 書中叫做 “Consolidate Conditional Expression”。 2
Refactor 到這裡,如果你還不滿意,還可以再套用一次 “Replace Nested Conditional with Guard Clauses” 手法:
def demo5():
for item in data: # A
if not condition1(item): # B1 + B2
print('CASE 3')
continue
for pair in item.items(): # C
print(pair)
if not condition6(pair): # D + E
continue
if condition4(pair): # F1
print('CASE 1')
elif condition5(pair): # F2
print('CASE 2')
需不需要做到這地步?
看情況。反正整段程式的邏輯結構已經很清晰易讀了,cyclomatic complexity 也已經降得很低了:
% radon cc -s demo.py
demo.py
F 11:0 demo1 - B (8)
F 26:0 demo2 - B (8)
F 42:0 demo3 - B (8)
F 57:0 demo4 - B (7)
F 72:0 demo5 - B (7)
ChatGPT 怎麼說
聽說 ChatGPT 都快要取代程式設計師了,我們就來看看 ChatGPT 會怎麼 refactor 第一段程式。
Prompt: Refactor the following Python code with less nested structure.
大體上方向是對的,只是在 condition3 ~ condition5 的地方疑似因為 Python 縮排判斷錯誤。
嗯,大家真的要加油了,不要輸給 ChatGPT 呀。
-
如果你手邊沒有 Refactoring 這本書,可參考以下兩篇文章學習 “Replace Nested Conditional with Guard Clauses” 手法:文章 1 & 文章 2 ↩︎
-
如果你手邊沒有 Refactoring 這本書,可參考以下兩篇文章學習 “Consolidate Conditional Expression” 手法:文章 1 & 文章 2。 ↩︎