你有聽過「蜥蜴腦」嗎?如果你讀過 The Pragmatic Programmer,你應該還有印象。蜥蜴腦是大腦中最原始的部分,為甚麼叫蜥蜴腦呢?因為人腦的這個部分和蜥蜴並無二致:肚子餓時都會找食物,受到攻擊,或是感覺有危險時,都會戰鬥或逃跑,而且要快。蜥蜴腦的運作,我們感覺不到,但它卻對我們的行為影響甚鉅。
寫程式時,理性思考固然不可少,但聽從「蜥蜴腦」的指示也很重要。筆者經常跟團隊說:「寫程式時,任何時候寫到 If 都要十分警惕,要停下來想一下。」這不是開玩笑,這是真的。任何 If 都可能是短時間內劇烈破壞你程式整潔的轉折點,要非常慎重地使用。
於是,我們終於來到 Code Smell 系列的最後一篇。今天要來聊的,是古今中外無人能出其右,人稱味道之王的「If」。
等等,為什麼 If 是個壞味道?不是每個程式語言都有 If-else 嗎?難道這些程式語言的發明者都挖坑給我們跳?非也,非也,就像先前講 Data Class 一樣,不是所有只含資料不含邏輯的類別都是壞味道,If 也是一樣,要看上下文與應用場景,才能判斷出這個 If 是不是壞味道。
If 之所以為壞味道,主要體現在對「SRP - Single Responsibility Principle」的破壞。一個方法根據傳入的參數,如果有兩個以上的邏輯路徑,那就代表這方法做了兩件事。例如舉個常見的生活場景,「如果下雨就待在家,不然就出門」這句話,根據實際狀況是下雨還是不下雨,定義了「待在家」或是「出門」的兩件事。這就直接違反了單一職責原則。
當然,不是所有 If 都非修改不可,譬如「衛語句」是解決巢狀判斷式(Nested-if)的模式,但因為要判斷是否為異常,所以免不了必須要有個 If,這是沒辦法的事,但只要衛語句後面的「正常情況」只做一件事,這個 If 一般就不被認為是個壞味道,可以說是「利大於弊」的一種設計。你知道的,巢狀判斷式真的很討厭...。
利大於弊示意圖,圖片截自三立新聞網
Martin Fowler 在 Refactoring 書中,則是特別提到了 If 的難兄難弟:Switch Case。其實你知道我知道獨眼龍也知道,Switch Case 本質上就是 If-else,只是語法不同,但都是在迫使一個方法「做兩件以上的事情」。
獨眼龍也知道示意圖,圖片截自網路
Uncle Bob 在 Clean Code 中也有討論這件事,並對此表示:「我們要把 Switch Case盡可能消滅,但最後很有可能還是免不了要有一些簡單的 Switch Case,那我們就把它往下藏到主要邏輯之外。」
因此,對付 If 或是 Switch Case,常見的解法就是改採 State 或是 Strategy 設計模式,或是至少抽個介面,用多型的方式解決。然而,當你違反 SRP 情節不嚴重,範圍很小時,使用設計模式或多抽介面其實挺麻煩的,有種「大砲打小鳥」的感覺。更何況,有很多時候,If 的成因不是來自複雜的場景,而是來自單純的「新情況」。此時,我們也單純的採用「分別使用更精準方法」就挺好用的了。
譬如,當有個方法輸入參數有兩種可能性,而你會在方法中用 If 指定一種行為,else 指定另一種時,可以試著考慮把這個方法直接拆解成兩個只做一件事的方法,光是這樣,其實也就可以了。
在本系列的第 11 天文章中所演示的「依照不同學生身份,計算獎學金」的重構過程,其實就是用多型來重構 If 的例子,各位讀者不妨回頭再比較看看計算獎學金的邏輯,在改成多型的 Calculator 設計後,原先的主邏輯是否簡單很多。
除此之外,在下一篇,會有「使用狀態模式消除 If 壞味道」的範例,各位可以仔細看看這樣的壞味道,到底是怎麼在測試的保護下,被重構成耦合度較低的設計模式的。
這麼說起來,難不成以後「看到 If 就要把它換掉」?那倒未必。如同前文說的,If 的出現不一定代表做了兩件事。就算他真的做了兩件事,也是有情節輕與重之分。如果情節不嚴重,或是重構成本太高,其實也沒有什麼修改的必要,這時其實應該可以省下來,去做更重要的功能,不用糾結在這裡。
或是,可以考慮像筆者平常工作時一樣,反正有測試與 git 的雙重保護,我就花個五分鐘重構看看,如果沒什麼效果,或是反而變得不好閱讀,大不了 Reset 回來囉!不然裝 git 是為了什麼?
謎之聲:「人生不能重來,但 git 可以。」
ithelp2021