如何優化【if else】的面條代碼,我們一起來解決代碼復雜度問題!


相信不少同學在維護老項目時,都遇到過在深深的 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編程、游戲編程、黑客等等......


 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM