相信不少同學在維護老項目時,都遇到過在深深的 if else 之間糾纏的業務邏輯。面對這樣的一團亂麻,簡單粗暴地繼續增量修改常常只會讓復雜度越來越高,可讀性越來越差,有沒有固定的套路來梳理它呢?這里分享三種簡單通用的重構方式。
什么是面條代碼
所謂的【面條代碼】,常見於對復雜業務流程的處理中。它一般會滿足這么幾個特點:
✿ 內容長
✿ 結構亂
✿ 嵌套深
我們知道,主流的編程語言均有函數或方法來組織代碼。對於面條代碼,不妨認為它就是滿足這幾個特征的函數吧。根據語言語義的區別,可以將它區分為兩種基本類型:
if...if 型
這種類型的代碼結構形如:
function demo (a, b, c) { if (f(a, b, c)) { if (g(a, b, c)) { // ... } // ... if (h(a, b, c)) { // ... } } if (j(a, b, c)) { // ... } if (k(a, b, c)) { // ... } }
流程圖形如:

它通過從上到下嵌套的 if,讓單個函數內的控制流不停增長。不要以為控制流增長時,復雜度只會線性增加。
我們知道函數處理的是數據,而每個 if 內一般都會有對數據的處理邏輯。
那么,即便在不存在嵌套的情形下,如果有 3 段這樣的 if,那么根據每個 if 是否執行,數據狀態就有 2 ^ 3 = 8 種。
如果有 6 段,那么狀態就有 2 ^ 6 = 64 種。從而在項目規模擴大時,函數的調試難度會指數級上升!這在數量級上,與《人月神話》的經驗一致。
else if...else if 型
這個類型的代碼控制流,同樣是非常常見的。形如:
function demo (a, b, c) { if (f(a, b, c)) { if (g(a, b, c)) { // ... } // ... else if (h(a, b, c)) { // ... } // ... } else if (j(a, b, c)) { // ... } else if (k(a, b, c)) { // ... } }
流程圖形如:

else if 最終只會走入其中的某一個分支,因此並不會出現上面組合爆炸的情形。但是,在深度嵌套時,復雜度同樣不低。
假設嵌套 3 層,每層存在 3 個 else if,那么這時就會出現 3 ^ 3 = 27 個出口。
如果每種出口對應一種處理數據的方式,那么一個函數內封裝這么多邏輯,也顯然是違背單一職責原則的。並且,上述兩種類型可以無縫組合,進一步增加復雜度,降低可讀性。
但為什么在這個有了各種先進的框架和類庫的時代,還是經常會出現這樣的代碼呢?
個人的觀點是,復用的模塊確實能夠讓我們少寫【模板代碼】,但業務本身無論再怎么封裝,也是需要開發者去編寫邏輯的。而即便是簡單的 if else,也能讓控制流的復雜度指數級上升。
從這個角度上說,如果沒有基本的編程素養,不論速成掌握再優秀的框架與類庫,同樣會把項目寫得一團糟。
重構策略
上文中,我們已經討論了面條代碼的兩種類型,並量化地論證了它們是如何讓控制流復雜度指數級激增的。然而,在現代的編程語言中,這種復雜度其實是完全可控的。下面分幾種情形,列出改善面條代碼的編程技巧。
基本情形
對看起來復雜度增長最快的 if...if 型面條代碼,通過基本的函數即可將其拆分。下圖中每個綠框代表拆分出的一個新函數:

由於現代編程語言摒棄了 goto,因此不論控制流再復雜,函數體內代碼的執行順序也都是從上而下的。
因此,我們完全有能力在不改變控制流邏輯的前提下,將一個單體的大函數,自上而下拆逐步分為多個小函數,而后逐個調用之。這是有經驗的同學經常使用的技巧,具體代碼實現在此不做贅述了。
需要注意的是,這種做法中所謂的不改變控制流邏輯,意味着改動並不需要更改業務邏輯的執行方式,只是簡單地【把代碼移出去,然后用函數包一層】而已。有些同學可能會認為這種方式治標不治本,不過是把一大段面條切成了幾小段,並沒有本質的區別。
然而真的是這樣嗎?通過這種方式,我們能夠把一個有 64 種狀態的大函數,拆分為 6 個只返回 2 種不同狀態的小函數,以及一個逐個調用它們的 main 函數。這樣一來,每個函數復雜度的增長速度,就從指數級降低到了線性級。
這樣一來,我們就解決了 if...if 類型面條代碼了,那么對於 else if...else if 類型的呢?
查找表
對於 else if...else if 類型的面條代碼,一種最簡單的重構策略是使用所謂的查找表。它通過鍵值對的形式來封裝每個 else if 中的邏輯:
const rules = { x: function (a, b, c) { /* ... */ }, y: function (a, b, c) { /* ... */ }, z: function (a, b, c) { /* ... */ } } function demo (a, b, c) { const action = determineAction(a, b, c) return rules[action](a, b, c) }
每個 else if 中的邏輯都被改寫為一個獨立的函數,這時我們就能夠將流程按照如下所示的方式拆分了:

對於先天支持反射的腳本語言來說,這也算是較為 trivial 的技巧了。但對於更復雜的 else if 條件,這種方式會重新把控制流的復雜度集中到處理【該走哪個分支】問題的 determineAction 中。有沒有更好的處理方式呢?
職責鏈模式
在上文中,查找表是用鍵值對實現的,對於每個分支都是 else if (x === 'foo') 這樣簡單判斷的情形時,'foo' 就可以作為重構后集合的鍵了。
但如果每個 else if 分支都包含了復雜的條件判斷,且其對執行的先后順序有所要求,那么我們可以用職責鏈模式來更好地重構這樣的邏輯。
對 else if 而言,注意到每個分支其實是從上到下依次判斷,最后僅走入其中一個的。
這就意味着,我們可以通過存儲【判定規則】的數組,來實現這種行為。如果規則匹配,那么就執行這條規則對應的分支。我們把這樣的數組稱為【職責鏈】,這種模式下的執行流程如下圖:

在代碼實現上,我們可以通過一個職責鏈數組來定義與 else if 完全等效的規則:
const rules = [ { match: function (a, b, c) { /* ... */ }, action: function (a, b, c) { /* ... */ } }, { match: function (a, b, c) { /* ... */ }, action: function (a, b, c) { /* ... */ } }, { match: function (a, b, c) { /* ... */ }, action: function (a, b, c) { /* ... */ } } // ... ]
rules 中的每一項都具有 match 與 action 屬性。這時我們可以將原有函數的 else if 改寫對職責鏈數組的遍歷:
function demo (a, b, c) { for (let i = 0; i < rules.length; i++) { if (rules[i].match(a, b, c)) { return rules[i].action(a, b, c) } } }
這時每個職責一旦匹配,原函數就會直接返回,這也完全符合 else if 的語義。通過這種方式,我們就實現了對單體復雜 else if 邏輯的拆分了。
總結
面條代碼其實容易出現在不加思考的【糙快猛】式開發中。很多簡單粗暴地【在這里加個 if,在那里多個 return】的 bug 修復方式,再加上注釋的匱乏,很容易讓代碼可讀性越來越差,復雜度越來越高。
在實現常見業務功能時,掌握好編程語言,梳理好需求,用最簡單的代碼將其實現,就已經是最優解了。
不管你是轉行也好,初學也罷,進階也可——【值得關注進入】的編程學習進階俱樂部
涉及到:C語言、C++、windows編程、網絡編程、QT界面開發、Linux編程、游戲編程、黑客等等......

一個活躍、高格調、高層次的程序員編程學習殿堂;編程入門只是順帶,思維的提高才有價值!