讀別人的程式是一件苦差事,不僅需要具備對應的領域知識,也得了解對方的程式書寫邏輯與風格。因此,為了讓程式碼容易被人理解,整潔程式碼是必須持續精進的技藝。

在 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 呀。


  1. 如果你手邊沒有 Refactoring 這本書,可參考以下兩篇文章學習 “Replace Nested Conditional with Guard Clauses” 手法:文章 1 & 文章 2 ↩︎

  2. 如果你手邊沒有 Refactoring 這本書,可參考以下兩篇文章學習 “Consolidate Conditional Expression” 手法:文章 1 & 文章 2。 ↩︎