降低程式碼圈複雜度最佳化技巧

發表於2023-09-20

當一個專案經過持續迭代,不斷增加功能,逐漸變成一個複雜的產品時,新功能的開發變得相對困難。其中一個很大的原因是程式碼複雜度高,導致可維護性和可讀性都很差。本文將從前端JavaScript的角度出發,介紹一些有效的方法和技巧來最佳化前端程式碼的圈複雜度

什麼是圈複雜度

圈複雜度的計算基於程式中的決策結構,如條件語句(if語句)、迴圈語句(for、while語句)、分支語句(switch語句)等。每當程式流程圖中增加一個決策點,圈複雜度就會增加1。圈複雜度的值越高,表示程式碼的複雜性越大,程式碼的可讀性、可測性和可維護性也會受到影響。

通常情況下,圈複雜度的推薦值應該在1到10之間。超過10的程式碼模組可能需要進行重構,以提高程式碼的可理解性和可測試性,並降低引入錯誤的風險。

輔助工具

VScode外掛Code Metrics

VScode外掛Code Metrics可以幫助我們快速發現那些需要最佳化複雜度的程式碼,安裝好外掛後如下圖所示,在程式碼上方會出現對應的複雜度值,根據值的大小可以看出哪些程式碼是急需最佳化提升可讀性。

滑鼠點選所提示覆雜度數值的地方可以看到具體是哪些程式碼影響了複雜度,可以進行針對性的最佳化。

eslint檢查

可以使用 eslint 幫助檢查程式碼的圈複雜度,當超出了某個值就會報錯。

rules: {
  complexity: [
    'error',
    {
      max: 10
    }
  ]
}

如上面的配置就是超出了 10 就會出現報錯資訊。

圈複雜度的常用解決方法

函式拆分和重構,單一職責

較高的圈複雜度往往意味著函式或方法內部有過多的決策路徑。透過將複雜的函式分解成多個小而清晰的函式,可以降低每個函式的圈複雜度,並使程式碼更易於理解和維護。拆分函式時,可根據功能模組或責任進行分類,確保每個函式只負責一項具體的任務。

最佳化前程式碼:

function handle(arr) {
    // 去重
    let _arr=[],_arrIds=[];
    for(let i=0;i<arr.length;i++){
        if(_arrIds.indexOf(arr[i].id)===-1){
            _arrIds.push(arr[i].id);
            _arr.push(arr[i]);
        }
    }
    // 替換
    _arr.map(item=>{
        for(let key in item){
            if(item[key]===''){
                item[key]='--';
            }
        }
    });
    // 排序
    _arr.sort((item1,item2)=>item1.id-item2.id);
    return _arr;
}

最佳化後程式碼:

function removeDuplicates(arr) {
  const uniqueArr = [];
  const uniqueIds = [];
  
  for(let i = 0; i < arr.length; i++) {
    if(uniqueIds.indexOf(arr[i].id) === -1) {
      uniqueIds.push(arr[i].id);
      uniqueArr.push(arr[i]);
    }
  }
  
  return uniqueArr;
}

function replaceEmptyValues(arr) {
  const processedArr = arr.map(item => {
    for(let key in item) {
      if(item[key] === '') {
        item[key] = '--';
      }
    }
    return item;
  });
  
  return processedArr;
}

function sortById(arr) {
  const sortedArr = arr.sort((item1, item2) => item1.id - item2.id);
  return sortedArr;
}

function handle(arr) {
  const uniqueArr = removeDuplicates(arr);
  const processedArr = replaceEmptyValues(uniqueArr);
  const sortedArr = sortById(processedArr);
  return sortedArr;
}

以上將原始函式拆分成了三個函式。removeDuplicates 函式用於去除陣列中的重複元素,replaceEmptyValues 函式用於遍歷替換空值,sortById 函式用於根據 id 進行排序。每個函式都只負責一個明確的職責。

衛語句可以減少分支

對輸入條件進行多重判斷時,使用衛語句可以減少分支語句的使用,提高程式碼的可讀性和可維護性。

// 最佳化前
function calculateScore(score) {
  if (score < 0) {
    return "Invalid score";
  } else if (score < 50) {
    return "Fail";
  } else if (score < 70) {
    return "Pass";
  } else if (score < 90) {
    return "Good";
  } else {
    return "Excellent";
  }
}

// 最佳化後
function calculateScore(score) {
  if (score < 0) {
    return "Invalid score";
  }
  if (score < 50) {
    return "Fail";
  }
  if (score < 70) {
    return "Pass";
  }
  if (score < 90) {
    return "Good";
  }
  return "Excellent";
}

透過使用衛語句,我們將每個條件判斷獨立出來,避免了巢狀的分支語句。這種最佳化方式使得程式碼更加清晰,每個條件判斷都獨立成為一個邏輯塊,並且消除了使用 else 的需要。這樣做不僅提高了程式碼的可讀性,還方便了後續對每個條件判斷的修改和維護。

簡化條件表示式

有相同邏輯程式碼進行條件合併輸出,減少條件判斷程式碼,提升可讀性。

// 最佳化前
function a (num) {
    if (num === 0) {
        return 0;
    } else if (num === 1) {
        return 1;
    } else if (num === 2) {
        return 2;
    } else {
        return 3;
    }
}

// 最佳化後
function a (num) {
    if ([0, 1, 2].indexOf(num) > -1) {
        return num;
    } else {
        return 3;
    }
}
---
// 最佳化前
function a() {
  if (this.a == 0) return;
  if (!this.b) return;
  ...
}

// 最佳化後
function a() {
  if (this.a == 0 || !this.b) return;
  ...
}
---
// 最佳化前
function a (type) {
    if (type === 'a') {
        return 'String';
    } else if (type === 'b') {
        return 'Number';
    } else if (type === 'c') {
        return 'Object';
    }
}

// 最佳化後
function a (type) {
    let obj = {
        'a': 'String',
        'b': 'Number',
        'c': 'Object'
    };
    return obj[type];
}

表示式邏輯最佳化

邏輯計算也會增加圈複雜度,最佳化一些結構複雜的邏輯表示式,減少不必要的邏輯判斷,也將一定程度上降低圈複雜度。

// 最佳化前
a && b || a && c

// 最佳化後
a && (b || c)

透過多型方式替代條件式。

透過多型方式替代條件式是一種最佳化技巧,多型允許我們根據不同的型別執行不同的操作,而不需要使用複雜的條件判斷邏輯。

最佳化前的程式碼:

class Shape {
  constructor(type) {
    this.type = type;
  }

  calculateArea() {
    if (this.type === "circle") {
      // 計算圓形的面積
    } else if (this.type === "rectangle") {
      // 計算矩形的面積
    } else if (this.type === "triangle") {
      // 計算三角形的面積
    }
  }
}

最佳化後的程式碼:

class Shape {
  calculateArea() {
    throw new Error("calculateArea() method must be implemented");
  }
}

class Circle extends Shape {
  calculateArea() {
    // 計算圓形的面積
  }
}

class Rectangle extends Shape {
  calculateArea() {
    // 計算矩形的面積
  }
}

class Triangle extends Shape {
  calculateArea() {
    // 計算三角形的面積
  }
}

使用多型的方式,我們可以透過呼叫相應物件的calculateArea方法來執行特定形狀的面積計算,而無需使用複雜的條件判斷邏輯。

替換演算法,最佳化複雜度

當發現某個演算法的時間複雜度較高時,可以考慮替換為一個具有更優時間複雜度的演算法,以提高程式碼的效能。

// 最佳化前
function findDuplicates(nums) {
  let duplicates = [];
  for (let i = 0; i < nums.length; i++) {
    for (let j = i + 1; j < nums.length; j++) {
      if (nums[i] === nums[j]) {
        duplicates.push(nums[i]);
      }
    }
  }
  return duplicates;
}

// 最佳化後
function findDuplicates(nums) {
  let freq = {};
  let duplicates = [];
  for (let num of nums) {
    if (freq[num]) {
      duplicates.push(num);
    } else {
      freq[num] = true;
    }
  }
  return duplicates;
}

需要注意的是,最佳化演算法並不總是適用於所有情況。在選擇替代演算法時,應該綜合考慮資料規模、特定問題的特性以及演算法的複雜度等因素。

分解條件式,拆分函式

當遇到複雜的條件判斷式或函式時,可以考慮將其分解為更小的部分,以提高程式碼的可讀性和維護性。

最佳化前程式碼:

function calculateScore(player) {
  if (player.score >= 100 && player.level === "expert") {
    return player.score * 2;
  } else if (player.score >= 50 || player.level === "intermediate") {
    return player.score * 1.5;
  } else {
    return player.score;
  }
}

最佳化後程式碼:

function hasHighScore(player) {
  return player.score >= 100 && player.level === "expert";
}

function hasIntermediateScore(player) {
  return player.score >= 50 || player.level === "intermediate";
}

function calculateScore(player) {
  if (hasHighScore(player)) {
    return player.score * 2;
  } else if (hasIntermediateScore(player)) {
    return player.score * 1.5;
  } else {
    return player.score;
  }
}

將原始的複雜條件判斷式拆分成了兩個獨立的函式:hasHighScorehasIntermediateScore。這樣calculateScore函式中的條件判斷變得更加清晰和可讀。透過分解條件式和拆分函式,我們可以提高程式碼的可讀性、可維護性和重用性。

減少return出現

當前大多數圈複雜度計算工具對return個數也進行計算,如果要針對這些工具衡量規則進行最佳化,減少return語句個數也為一種方式。

// 最佳化前
function a(){
    const value = getSomething();
    if(value) {
        return true;
    } else {
        return false;
    }
}

// 最佳化後
function a() {
    return getSomething();
}

移除控制標記,減少變數

移除控制標記可以使程式碼更加簡潔、可讀性更高,並且減少了不必要的變數使用。

最佳化前的程式碼:

function findFirstPositive(numbers) {
  let found = false;
  let firstPositive = null;
  
  for (let num of numbers) {
    if (num > 0) {
      found = true;
      firstPositive = num;
      break;
    }
  }

  if (found) {
    return firstPositive;
  } else {
    return -1;
  }
}

最佳化後的程式碼:

function findFirstPositive(numbers) {
  for (let num of numbers) {
    if (num > 0) {
      return num;
    }
  }

  return -1;
}

在最佳化後的程式碼中,我們直接在找到第一個正數後立即返回結果,而無需使用控制標記和額外的變數。如果遍歷完整個陣列後仍未找到正數,則返回-1。

最後

如果只是刻板的使用圈複雜度的演算法去衡量一段程式碼的清晰度,這並不可取。在重構系統時,我們可以使用程式碼圈複雜度工具來統計程式碼的複雜度,並對複雜度較高的程式碼進行具體的場景分析。但不是說一定要將複雜度最佳化到某種程度,應該根據實際的業務情況做出最佳化決策。


看完本文如果覺得有用,記得點個贊支援,收藏起來說不定哪天就用上啦~

專注前端開發,分享前端相關技術乾貨,公眾號:南城大前端(ID: nanchengfe)

相關文章