如何無痛下降 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-before
if-if-before

它經過從上到下嵌套的 if,讓單個函數內的控制流不停增加。不要覺得控制流增加時,複雜度只會線性增長。咱們知道函數處理的是數據,而每一個 if 內通常都會有對數據的處理邏輯。那麼,即使在不存在嵌套的情形下,若是有 3 段這樣的 if,那麼根據每一個 if 是否執行,數據狀態就有 2 ^ 3 = 8 種。若是有 6 段,那麼狀態就有 2 ^ 6 = 64 種。從而在項目規模擴大時,函數的調試難度會指數級上升!這在數量級上,與《人月神話》的經驗一致。函數

else if...else if

這個類型的代碼控制流,一樣是很是常見的。形如:spa

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-before
else-if-before

else if 最終只會走入其中的某一個分支,所以並不會出現上面組合爆炸的情形。可是,在深度嵌套時,複雜度一樣不低。假設嵌套 3 層,每層存在 3 個 else if,那麼這時就會出現 3 ^ 3 = 27 個出口。若是每種出口對應一種處理數據的方式,那麼一個函數內封裝這麼多邏輯,也顯然是違背單一職責原則的。而且,上述兩種類型能夠無縫組合,進一步增長複雜度,下降可讀性。3d

但爲何在這個有了各類先進的框架和類庫的時代,仍是常常會出現這樣的代碼呢?我的的觀點是,複用的模塊確實可以讓咱們少寫【模板代碼】,但業務自己不管再怎麼封裝,也是須要開發者去編寫邏輯的。而即使是簡單的 if else,也能讓控制流的複雜度指數級上升。從這個角度上說,若是沒有基本的編程素養,不論速成掌握再優秀的框架與類庫,一樣會把項目寫得一團糟調試

重構策略

上文中,咱們已經討論了麪條代碼的兩種類型,並量化地論證了它們是如何讓控制流複雜度指數級激增的。然而,在現代的編程語言中,這種複雜度實際上是徹底可控的。下面分幾種情形,列出改善麪條代碼的編程技巧。

基本情形

對看起來複雜度增加最快的 if...if 型麪條代碼,經過基本的函數便可將其拆分。下圖中每一個綠框表明拆分出的一個新函數:

if-if-after
if-if-after

因爲現代編程語言摒棄了 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 中的邏輯都被改寫爲一個獨立的函數,這時咱們就可以將流程按照以下所示的方式拆分了:

else-if-lookup
else-if-lookup

對於先天支持反射的腳本語言來講,這也算是較爲 trivial 的技巧了。但對於更復雜的 else if 條件,這種方式會從新把控制流的複雜度集中處處理【該走哪一個分支】問題的 determineAction 中。有沒有更好的處理方式呢?

職責鏈模式

在上文中,查找表是用鍵值對實現的,對於每一個分支都是 else if (x === 'foo') 這樣簡單判斷的情形時,'foo' 就能夠做爲重構後集合的鍵了。但若是每一個 else if 分支都包含了複雜的條件判斷,且其對執行的前後順序有所要求,那麼咱們能夠用職責鏈模式來更好地重構這樣的邏輯。

else if 而言,注意到每一個分支實際上是從上到下依次判斷,最後僅走入其中一個的。這就意味着,咱們能夠經過存儲【斷定規則】的數組,來實現這種行爲。若是規則匹配,那麼就執行這條規則對應的分支。咱們把這樣的數組稱爲【職責鏈】,這種模式下的執行流程以下圖:

else-if-chain
else-if-chain

在代碼實現上,咱們能夠經過一個職責鏈數組來定義與 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 中的每一項都具備 matchaction 屬性。這時咱們能夠將原有函數的 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 修復方式,再加上註釋的匱乏,很容易讓代碼可讀性愈來愈差,複雜度愈來愈高。

但解決這個問題的幾種方案都並不複雜。這些示例之因此簡單,本質上是由於高級編程語言強大的表達能力已經可以不依賴於各類模式的模板代碼,爲需求提供直接的語義支持,而無需套用各類設計模式的八股文。

固然,你能夠用模式來歸納一些下降業務邏輯複雜度的技巧,但若是生搬硬套地記憶並使用模式,一樣可能會走進過分設計的歧途。在實現常見業務功能時,掌握好編程語言,梳理好需求,用最簡單的代碼將其實現,就已是最優解了。

相關文章
相關標籤/搜索