淺談前端中的圈複雜度

DevUI是一支兼具設計視角和工程視角的團隊,服務於華爲雲DevCloud平臺和華爲內部數箇中後臺系統,服務於設計師和前端工程師。
官方網站:devui.design
Ng組件庫:ng-devui(歡迎Star)前端

引言

重構,是咱們開發過程當中不可避免須要進行的一項工做。重構代碼,以適配當前模塊設計之初未考慮到的多樣化場景,並增長模塊的可維護性、健壯性、可測試性。那麼,如何明確重構的方向,以及量化重構的結果呢?git

代碼圈複雜度(Cyclomatic complexity,CC)能夠是一個供選擇的指標。github

什麼是圈複雜度

圈複雜度(Cyclomatic complexity,CC)也稱爲條件複雜度或循環複雜度,是一種軟件度量,是由老托馬斯·J·麥凱布(Thomas J. McCabe, Sr.)在1976年提出,用來表示程序的複雜度,其符號爲VG或是M。圈複雜度即程序的源代碼中線性獨立路徑的個數。算法

爲什麼要下降模塊(函數)的圈複雜度

下表爲模塊(函數)圈複雜度與代碼情況的一個基本對照表。除了表中給出的代碼情況、可測性、維護成本等指標外,圈複雜度高的模塊(函數),也對應着高軟件複雜度、低內聚、高風險、低可讀性。咱們要下降模塊(函數)的圈複雜度,就是要下降其軟件複雜度、增長內聚性、減小可能出現的軟件缺陷個數、加強可測試性、可讀性。markdown

圈複雜度前端工程師

代碼情況框架

可測性函數

維護成本工具

1 - 10oop

清晰、結構化

10 - 20

複雜

20 - 30

很是複雜

>30

不可讀

不可測

很是高

下降軟件複雜度

麥凱布提出圈複雜度時,其原始目的之一就是但願在軟件開發過程當中就限制其複雜度。他建議程序設計者需計算其開發模塊的複雜度,若一模塊的圈複雜度超過10,需再分割爲更小的模塊。NIST(國家標準技術研究所)的結構化測試方法論已此做法略做調整,在一些特定情形下,模塊圈複雜度上限放寬到15會比較合適。此方法論也認可有些特殊情形下,模塊的複雜度須要超過上述的上限,其建議爲「模塊的循環複雜度需在上限範圍之內,不然需提供書面數據,說明爲什麼此模塊循環複雜度有必要超過上限。」

增長模塊內聚性

優秀的代碼模塊間老是低耦合,高內聚的。能夠預期一個複雜度較高模塊的內聚性會比較低,至少不會到功能內聚性的程度。一個有高複雜度及低內聚性的模塊中會有許多的決策點,這類的模塊多半運行超過一個明肯定義的任務,所以內聚性較低。

減小可能出現的軟件缺陷個數

許多研究指出模塊(函數)的圈複雜度和其中的缺陷個數有相關性,許多這類研究發現圈複雜度和缺陷個數有高度的正相關:圈複雜度最高的模塊及方法,其中的缺陷個數也最多。

加強模塊可測試性

一個圈複雜度高的模塊(函數),由下文中將描述到的計算方法來看,必然會有更多的運行分支,要對這樣的模塊進行如單元測試用例的編寫,將會十分複雜,而且後期用例維護也是一個問題。

加強代碼可讀性

代碼可讀性是大型項目與團隊協做間必需要考慮的一個因素。圈複雜度高的模塊(函數),隨着邏輯複雜度的增長,代碼可讀性也將下降,不利於成員間相互協做與後期維護。

圈複雜度如何計算

計算方法

一段程序的圈複雜度是其線性獨立路徑的數量。若程序中沒有像IF指令或FOR循環的控制流程,由於程序中只有一個路徑,其圈複雜度爲1,若程序中有一個IF指令,會有二個不一樣路徑,分別對應IF條件成立及不成立的情形,所以圈複雜度爲2。

數學上,一個結構化程序的圈複雜度是利用程序的控制流圖來定義,控制流圖是一個有向圖,圖中的節點爲程序的基礎模塊,若一個模塊結束後,可能會運行另外一個模塊,則用箭頭連接二個模塊,並標示可能的運行順序。圈複雜度M能夠用下式定義:

M = E − N + 2P

其中

E 爲圖中邊的個數

N 爲圖中節點的個數

P 爲鏈接組件的個數

度量工具

CodeMetrics

一款VSCode插件,用於度量TS、JS代碼圈複雜度。

ESLint

eslint也能夠配置關於圈複雜度的規則,如:

rules: { 
  complexity: [ 
    'error', 
    { 
      max: 15 
    } 
  ] 
}
複製代碼

表明了當前每一個函數最高圈複雜度爲15,不然eslint將給出錯誤提示

conard cc

一款開源的代碼圈複雜度檢測工具(github:github.com/ConardLi/aw…),能夠生成當前項目下代碼圈複雜度報告。

如何下降模塊(函數)圈複雜度

經常使用結構圈複雜度

要下降圈複雜度,咱們就須要瞭解是哪些語句哪些結構致使了咱們複雜度的增長,如下爲常見結構圈複雜度說明。

順序結構

順序結構複雜度爲1。

例:

function func() {
  let a = 1, b = 1, c;
  c = a * b;
}
複製代碼

如上代碼,func函數內部爲順序結構,其控制流圖以下:

邊:1,點:2,連通分支:1,

圈複雜度:

M = 1 - 2 + 2 * 1 = 1
複製代碼

if-else-else、switch-case

每增長一個分支,複雜度增長1,&& 、|| 運算也爲一個分支。

例:

function func() {
  let a = 1, b = 1, c;
  if (a = 1) {
    c = a + b;
  } else {
    c = a - b;
  }
}
複製代碼

邊:4,點:4,連通分支:1,

圈複雜度:

M = 4 - 4 + 2 * 1 = 2
複製代碼

循環結構

增長一個循環結構,複雜度增長1。、

例:

function func() {
  let a = 1, b = 1, c = 2;
  for (let i = 1; i < 10; i++) {
    b += a;
  }
  c = a + b;
}
複製代碼

邊:4,點:4,連通分支:1,

圈複雜度:

M = 4 - 4 + 2 * 1 = 2
複製代碼

return

從理論上來說,return並不會增長當前模塊圈複雜度,但在某些度量工具看來,一條return語句將增長總體程序的一條路徑,而且若是提早返回,將增長程序的不肯定性,因此在大多數計算工具中,每增長一條return語句,複雜度將加1。

經常使用下降模塊(函數)圈複雜度方法

1. 函數提煉與拆分,單一職責(推薦)

既然是下降一個模塊(函數)圈複雜度,那麼對於複雜度極高的函數,首先須要進行就是功能的提煉與函數拆分,每一個函數職責要單一。

例:

function add(a, b) {
  let tempA;
  if (a === 10) {
    tempA = 11;
  } else if (a === 12) {
    tempA = 12;
  }
  let tempB;
  if (b === 10) {
    tempB = 13;
  } else if (b === 12) {
    tempB = 12;
  }
  return tempB + tempA;
}
複製代碼

重構爲:

function add(a, b) {
  return calcA(a) + calcB(b);
}

function calcA(a) {
  if (a === 10) {
    return 11;
  } else if (a === 12) {
    return 12;
  }
}

function calcB(b) {
  if (b === 10) {
    return 13;
  } else if (b === 12) {
    return 12;
  }
}
複製代碼

不只下降了add函數圈複雜度,而且代碼結構更加清晰,增長了可讀性,同時還增長了當前代碼可維護性、可測試性。

固然,過猶不及,咱們的目標爲提煉函數,保持函數單一職責,不能爲了下降圈複雜度而進行暴力拆分。

2. 優化算法(減小沒必要要條件、循環分支)

從圈複雜度計算上來看,條件、循環分支均會增長模塊圈複雜度。從某些程度上,複雜的條件與循環結構是可優化,減小沒必要要結構,從而下降圈複雜度。

例:

let a = 'a', c;
if (a === 'a') {
  c = a + 1;
} else if (a === 'b') {
  c = a + 2;
} else if (a === 'c') {
  c = a + 3;
} else if (a === 'd') {
  c = a + 4;
}
return c;
複製代碼

重構爲:

let a = 'a', c;
let conditionMap = {
  a: 1,
  b: 2,
  c: 3,
  d: 4
}
c = a + conditionMap[a];
return c;
複製代碼

消除了全部條件分支,從而大幅下降了當前函數圈複雜度。

3. 表達式邏輯優化

邏輯計算也將增長圈複雜度,優化一些結構複雜的邏輯表達式,減小沒必要要的邏輯判斷,也將必定程度上下降圈複雜度。

例:

a && b || a && c
複製代碼

可進行簡單優化爲:

a && (b || c)
複製代碼

從而使表達式圈複雜度下降1。

5. 減小提早return(此方法需辯證看待)

單從下降圈複雜度上來看,因爲當前大多數圈複雜度計算工具將對return個數進行計算,故若要針對這些工具衡量規則進行優化,減小return語句個數也爲一種方式。

例:

let a = 1, b = 1;
if (a = 1) {
  return a + b;
} else {
  return a - b;
}
複製代碼

重構爲:

let a = 1, b = 1, c;
if (a = 1) {
  c = a + b;
} else {
  c = a - b;
}
return c;
複製代碼

圈複雜度將下降1。

總結

圈複雜度(Cyclomatic complexity,CC)高的代碼必定不是好代碼,對於咱們代碼好壞的衡量,圈複雜度能夠做爲一個參考指標;能夠經過控制流圖計算圈複雜度;要下降模塊(函數)圈複雜度,提煉拆分函數、優化算法、優化邏輯表達式均爲能夠嘗試的方法。

參考

循環複雜度:zh.wikipedia.org/wiki/%E5%BE…

詳解圈複雜度:kaelzhang81.github.io/2017/06/18/…

前端代碼質量-圈複雜度原理和實踐:juejin.cn/post/684490…

加入咱們

咱們是DevUI團隊,歡迎來這裏和咱們一塊兒打造優雅高效的人機設計/研發體系。招聘郵箱:muyang2@huawei.com

文/DevUI 砰砰砰砰

往期文章推薦

手把手教你搭建本身的Angular組件庫

《手把手教你使用Vue/React/Angular三大框架開發Pagination分頁組件》

《手把手教你搭建一個灰度發佈環境》

相關文章
相關標籤/搜索