背景
程式碼的複雜度是評估一個專案的重要標準之一。較低的複雜度既能減少專案的維護成本,又能避免一些不可控問題的出現。然而在日常的開發中卻沒有一個明確的標準去衡量程式碼結構的複雜程度,大家只能憑著經驗去評估程式碼結構的複雜程度,比如,程式碼的程度、結構分支的多寡等等。當前程式碼的複雜度到底是個什麼水平?什麼時候就需要我們去優化程式碼結構、降低複雜度?這些問題我們不得而知。
因此,我們需要一個明確的標準去衡量程式碼的複雜度。
衡量標準
Litmus
是我們團隊建設的一個程式碼質量檢測系統,目前包括程式碼的風格檢查、重複率檢查以及複雜度檢查。litmus 採用程式碼的 Maintainability(可維護性)來衡量一個程式碼的複雜度,並且通過以下三個方面來定義一段程式碼的 Maintainability 的值:
- Halstead Volume(程式碼容量)
- Cyclomatic Complexity(圈複雜度)
- Lines of Code(程式碼行數)
根據這三個引數計算出 Maintainability,也就是程式碼的可維護性,公式如下:
Maintainability Index = MAX(0,(171 - 5.2 * ln(Halstead Volume) - 0.23 * (Cyclomatic Complexity) - 16.2 * ln(Lines of Code))*100 / 171)複製程式碼
程式碼行數不做贅述,下面我們具體介紹程式碼容量、圈複雜的含義以及它們的計算原理
Halstead Volume(程式碼容量)
程式碼的容量關注的是程式碼的詞彙數,有以下幾個基本概念
引數 | 含義 |
---|---|
n1 | Number of unique operators,不同的操作元(運運算元)的數量 |
n2 | Number of unique operands,不同的運算元(運算元)的數量 |
N1 | Number of total occurrence of operators,為所有操作元(運運算元)合計出現的次數 |
N2 | Number of total occurrence of operands,為所有運算元(運算元)合計出現的次數 |
Vocabulary | n1 + n2,詞彙數 |
length | N1 + N2,長度 |
Volume | length * Log2 Vocabulary,容量 |
一個例子
function tFunc(opt) {
let result = opt + 1;
return result;
}
// n1:function,let,=,+,return
// n2:tFunc,opt,result,1
// N1: function,let,=,+,return
// N2:tFunc,opt,result,opt,1,result
// Vocabulary = n1 + n2 = 9
// length = N1 + N2 = 11
// Volume = length * Log2 Vocabulary = 34.869複製程式碼
Cyclomatic Complexity(圈複雜度)
概念
圈複雜度(Cyclomatic complexity,簡寫CC)也稱為條件複雜度,是一種程式碼複雜度的衡量標準。由托馬斯·J·麥凱布(Thomas J. McCabe, Sr.)於1976年提出,用來表示程式的複雜度,其符號為VG或是M。它可以用來衡量一個模組判定結構的複雜程度,數量上表現為獨立現行路徑條數,也可理解為覆蓋所有的可能情況最少使用的測試用例數。圈複雜度大說明程式程式碼的判斷邏輯複雜,可能質量低且難於測試和 維護。程式的可能錯誤和高的圈複雜度有著很大關係。
如何計算
如果在控制流圖中增加了一條從終點到起點的路徑,整個流圖形成了一個閉環。圈複雜度其實就是在這個閉環中線性獨立迴路的個數。
如圖,線性獨立迴路有:
- e1→ e2 → e
- e1 → e3 → e
所以複雜度為2
對於簡單的圖,我們還可以數一數,但是對於複雜的圖,這種方法就不是明智的選擇了。
計算公式
V(G) = e – n + 2 * p複製程式碼
- e:控制流圖中邊的數量(對應程式碼中順序結構的部分)
- n:代表在控制流圖中的判定節點數量,包括起點和終點(對應程式碼中的分支語句)
- ps:所有終點只計算一次,即使有多個 return 或者 throw
- p:獨立元件的個數
幾種常見的語句控制流圖
一個例子
code
function test(index, string) {
let returnString;
if (index == 1) {
if (string.length < 2) {
return '分支1';
}
returnString = "returnString1";
} else if (index == 2) {
if (string.length < 5) {
return '分支2';
}
returnString = "returnString2";
} else {
return '分支3'
}
return returnString;
}複製程式碼
flow-chart
flow-graph
計算
e(邊):9
n(判定節點):6
p:1
V = e - n + 2 * p = 5複製程式碼
如何優化
主要針對圈複雜度
大方向:減少判斷分支和迴圈的使用
(下面某些例子可能舉的不太恰當,僅用以說明這麼一種方法)
提煉函式
// 優化前,圈複雜度4
function a (type) {
if (type === 'name') {
return `name:${type}`;
} else if (type === 'age') {
return `age:${type}`;
} else if (type === 'sex') {
return `sex:${type}`;
}
}
// 優化後,圈複雜度1
function getName () {
return `name:${type}`;
}
function getAge () {
return `age:${type}`;
}
function getSex () {
return `sex:${type}`;
}複製程式碼
表驅動
// 優化前,圈複雜度4
function a (type) {
if (type === 'name') {
return 'Ann';
} else if (type === 'age') {
return 11;
} else if (type === 'sex') {
return 'female';
}
}
// 優化後,圈複雜度1
function a (type) {
let obj = {
'name': 'Ann',
'age': 11,
'sex': 'female'
};
return obj[type];
}複製程式碼
簡化條件表示式
// 優化前,圈複雜度4
function a (num) {
if (num === 0) {
return 0;
} else if (num === 1) {
return 1;
} else if (num === 2) {
return 2;
} else {
return 3;
}
}
// 優化後,圈複雜度2
function a (num) {
if ([0,1,2].indexOf(num) > -1) {
return num;
} else {
return 3;
}
}複製程式碼
簡化函式
// 優化前,圈複雜度4
function a () {
let str = '';
for (let i = 0; i < 10; i++) {
str += 'a' + i;
}
return str
}
function b () {
let str = '';
for (let i = 0; i < 10; i++) {
str += 'b' + i;
}
return str
}
function c () {
let str = '';
for (let i = 0; i < 10; i++) {
str += 'c' + i;
}
return str
}
// 優化後,圈複雜度2
function a (type) {
let str = '';
for (let i = 0; i < 10; i++) {
str += type + i;
}
return str
}複製程式碼
檢測工具
- 本地檢測:es6-plato
npm install --save es6-plato es6-plato -r -d report ./複製程式碼
- litmus 質量檢測中心
該系統由我們團隊開發,目前僅限美團點評公司內部使用
,系統部分截圖如下