網(wǎng)站建設(shè)銷售實習(xí)報告臨沂網(wǎng)站建設(shè)
1. 導(dǎo)讀
你們是否也有過下面的想法?
- 重構(gòu)一個項目還不如新開發(fā)一個項目…
- 這代碼是誰寫的,我真想…
你們的項目中是否也存在下面的問題?
- 單個項目也越來越龐大,團隊成員代碼風(fēng)格不一致,無法對整體的代碼質(zhì)量做全面的掌控
- 沒有一個準(zhǔn)確的標(biāo)準(zhǔn)去衡量代碼結(jié)構(gòu)復(fù)雜的程度,無法量化一個項目的代碼質(zhì)量
- 重構(gòu)代碼后無法立即量化重構(gòu)后代碼質(zhì)量是否提升
針對上面的問題,本文的主角?圈復(fù)雜度?重磅登場,本文將從圈復(fù)雜度原理出發(fā),介紹圈復(fù)雜度的計算方法、如何降低代碼的圈復(fù)雜度,如何獲取圈復(fù)雜度,以及圈復(fù)雜度在公司項目的實踐應(yīng)用。
2. 圈復(fù)雜度
2.1 定義
圈復(fù)雜度?(Cyclomatic complexity) 是一種代碼復(fù)雜度的衡量標(biāo)準(zhǔn),也稱為條件復(fù)雜度或循環(huán)復(fù)雜度,它可以用來衡量一個模塊判定結(jié)構(gòu)的復(fù)雜程度,數(shù)量上表現(xiàn)為獨立現(xiàn)行路徑條數(shù),也可理解為覆蓋所有的可能情況最少使用的測試用例數(shù)。簡稱 CC 。其符號為 VG 或是 M 。
圈復(fù)雜度?在 1976 年由 Thomas J. McCabe, Sr. 提出。
圈復(fù)雜度大說明程序代碼的判斷邏輯復(fù)雜,可能質(zhì)量低且難于測試和維護。程序的可能錯誤和高的圈復(fù)雜度有著很大關(guān)系。
2.2 衡量標(biāo)準(zhǔn)
代碼復(fù)雜度低,代碼不一定好,但代碼復(fù)雜度高,代碼一定不好。
圈復(fù)雜度 | 代碼狀況 | 可測性 | 維護成本 |
---|---|---|---|
1 - 10 | 清晰、結(jié)構(gòu)化 | 高 | 低 |
10 - 20 | 復(fù)雜 | 中 | 中 |
20 - 30 | 非常復(fù)雜 | 低 | 高 |
>30 | 不可讀 | 不可測 | 非常高 |
3. 計算方法
3.1 控制流程圖
控制流程圖,是一個過程或程序的抽象表現(xiàn),是用在編譯器中的一個抽象數(shù)據(jù)結(jié)構(gòu),由編譯器在內(nèi)部維護,代表了一個程序執(zhí)行過程中會遍歷到的所有路徑。它用圖的形式表示一個過程內(nèi)所有基本塊執(zhí)行的可能流向, 也能反映一個過程的實時執(zhí)行過程。
下面是一些常見的控制流程:
3.2 節(jié)點判定法
有一個簡單的計算方法,圈復(fù)雜度實際上就是等于判定節(jié)點的數(shù)量再加上1。向上面提到的:if else
?、switch case
?、?for
循環(huán)、三元運算符等等,都屬于一個判定節(jié)點,例如下面的代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | function testComplexity(*param*) {let result = 1;if (param > 0) {result--;}for (let i = 0; i < 10; i++) {result += Math.random();}switch (parseInt(result)) {case 1:result += 20;break;case 2:result += 30;break;default:result += 10;break;}return result > 20 ? result : result; } |
上面的代碼中一共有1
個if
語句,一個for
循環(huán),兩個case
語句,一個三元運算符,所以代碼復(fù)雜度為?1+2+1+1+1=6
。另外,需要注意的是?|| 和 &&
?語句也會被算作一個判定節(jié)點,例如下面代碼的代碼復(fù)雜為3
:
1 2 3 4 5 6 7 | function testComplexity(*param*) {let result = 1;if (param > 0 && param < 10) {result--;}return result; } |
3.3 點邊計算法
1 | M = E ? N + 2P |
- E:控制流圖中邊的數(shù)量
- N:控制流圖中的節(jié)點數(shù)量
- P:獨立組件的數(shù)目
前兩個,邊和節(jié)點都是數(shù)據(jù)結(jié)構(gòu)圖中最基本的概念:
P代表圖中獨立組件的數(shù)目,獨立組件是什么意思呢?來看看下面兩個圖,左側(cè)為連通圖,右側(cè)為非連通圖:
- 連通圖:對于圖中任意兩個頂點都是連通的
一個連通圖即為圖中的一個獨立組件,所以左側(cè)圖中獨立組件的數(shù)目為1,右側(cè)則有兩個獨立組件。
對于我們的代碼轉(zhuǎn)化而來的控制流程圖,正常情況下所有節(jié)點都應(yīng)該是連通的,除非你在某些節(jié)點之前執(zhí)行了?return
,顯然這樣的代碼是錯誤的。所以每個程序流程圖的獨立組件的數(shù)目都為1,所以上面的公式還可以簡化為?M = E ? N + 2
?。
4. 降低代碼的圈復(fù)雜度
我們可以通過一些代碼重構(gòu)手段來降低代碼的圈復(fù)雜度。
重構(gòu)需謹(jǐn)慎,示例代碼僅僅代表一種思想,實際代碼要遠(yuǎn)遠(yuǎn)比示例代碼復(fù)雜的多。
4.1 抽象配置
通過抽象配置將復(fù)雜的邏輯判斷進行簡化。例如下面的代碼,根據(jù)用戶的選擇項執(zhí)行相應(yīng)的操作,重構(gòu)后降低了代碼復(fù)雜度,并且如果之后有新的選項,直接加入配置即可,而不需要再去深入代碼邏輯中進行改動:
4.2 單一職責(zé) - 提煉函數(shù)
單一職責(zé)原則(SRP)
:每個類都應(yīng)該有一個單一的功能,一個類應(yīng)該只有一個發(fā)生變化的原因。
在?JavaScript
?中,需要用到的類的場景并不太多,單一職責(zé)原則則是更多地運用在對象或者方法級別上面。
函數(shù)應(yīng)該做一件事,做好這件事,只做這一件事。 — 代碼整潔之道
關(guān)鍵是如何定義這 “一件事” ,如何將代碼中的邏輯進行抽象,有效的提煉函數(shù)有利于降低代碼復(fù)雜度和降低維護成本。
4.3 使用 break 和 return 代替控制標(biāo)記
我們經(jīng)常會使用一個控制標(biāo)記來標(biāo)示當(dāng)前程序運行到某一狀態(tài),很多場景下,使用?break
?和?return
?可以代替這些標(biāo)記并降低代碼復(fù)雜度。
4.4 用函數(shù)取代參數(shù)
setField
?和?getField
?函數(shù)就是典型的函數(shù)取代參數(shù),如果么有?setField、getField
?函數(shù),我們可能需要一個很復(fù)雜的?setValue、getValue
?來完成屬性賦值操作:
4.5 簡化條件判斷 - 逆向條件
某些復(fù)雜的條件判斷可能逆向思考后會變的更簡單。
4.6 簡化條件判斷 -合并條件
將復(fù)雜冗余的條件判斷進行合并。
4.7 簡化條件判斷 - 提取條件
將復(fù)雜難懂的條件進行語義化提取。
5. 圈復(fù)雜度檢測方法
5.1 eslint規(guī)則
eslint
提供了檢測代碼圈復(fù)雜度的rules
:
我們將開啟?rules?
中的?complexity
?規(guī)則,并將圈復(fù)雜度大于?0
?的代碼的?rule severity
?設(shè)置為?warn
?或?error
?。
1 2 3 4 5 6 | rules: {complexity: ['warn',{ max: 0 }] } |
這樣?eslint
?就會自動檢測出所有函數(shù)的代碼復(fù)雜度,并輸出一個類似下面的?message
。
1 2 3 4 | Method 'testFunc' has a complexity of 12. Maximum allowed is 0 Async function has a complexity of 6. Maximum allowed is 0. ... |
5.2 CLIEngine
我們可以借助?eslint
?的?CLIEngine
?,在本地使用自定義的?eslint
?規(guī)則掃描代碼,并獲取掃描結(jié)果輸出。
初始化?CLIEngine
?:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | const eslint = require('eslint');const { CLIEngine } = eslint;const cli = new CLIEngine({parserOptions: {ecmaVersion: 2018,},rules: {complexity: ['error',{ max: 0 }]} }); |
使用?executeOnFiles
?對指定文件進行掃描,并獲取結(jié)果,過濾出所有?complexity
?的?message
?信息。
1 2 3 4 5 6 7 8 9 10 11 | const reports = cli.executeOnFiles(['.']).results;for (let i = 0; i < reports.length; i++) {const { messages } = reports[i];for (let j = 0; j < messages.length; j++) {const { message, ruleId } = messages[j];if (ruleId === 'complexity') {console.log(message);}} } |
5.3 提取message
通過?eslint
?的檢測結(jié)果將有用的信息提取出來,先測試幾個不同類型的函數(shù),看看?eslint
?的檢測結(jié)果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | function func1() {console.log(1); }const func2 = () => {console.log(2); };class TestClass {func3() {console.log(3);} }async function func4() {console.log(1); } |
執(zhí)行結(jié)果:
1 2 3 4 | Function 'func1' has a complexity of 1. Maximum allowed is 0. Arrow function has a complexity of 1. Maximum allowed is 0. Method 'func3' has a complexity of 1. Maximum allowed is 0. Async function 'func4' has a complexity of 1. Maximum allowed is 0. |
可以發(fā)現(xiàn),除了前面的函數(shù)類型,以及后面的復(fù)雜度,其他都是相同的。
函數(shù)類型:
Function
?:普通函數(shù)Arrow function
?: 箭頭函數(shù)Method
?: 類方法Async function
?: 異步函數(shù)
截取方法類型:
1 2 3 4 5 6 | const REG_FUNC_TYPE = /^(Method |Async function |Arrow function |Function )/g;function getFunctionType(message) {let hasFuncType = REG_FUNC_TYPE.test(message);return hasFuncType && RegExp.$1; } |
將有用的部分提取出來:
1 2 3 4 5 6 7 | const MESSAGE_PREFIX = 'Maximum allowed is 1.'; const MESSAGE_SUFFIX = 'has a complexity of ';function getMain(message) {return message.replace(MESSAGE_PREFIX, '').replace(MESSAGE_SUFFIX, ''); } |
提取方法名稱:
1 2 3 4 5 | function getFunctionName(message) {const main = getMain(message);let test = /'([a-zA-Z0-9_$]+)'/g.test(main);return test ? RegExp.$1 : '*'; } |
截取代碼復(fù)雜度:
1 2 3 4 5 | function getComplexity(message) {const main = getMain(message);(/(\d+)\./g).test(main);return +RegExp.$1; } |
除了?message
?,還有其他的有用信息:
- 函數(shù)位置:獲取?
messages
?中的?line
?、column
?即函數(shù)的行、列位置 - 當(dāng)前文件名稱:
reports
?結(jié)果中可以獲取當(dāng)前掃描文件的絕對路徑?filePath
?,通過下面的操作獲取真實文件名:
1 | filePath.replace(process.cwd(), '').trim() |
- 復(fù)雜度等級,根據(jù)函數(shù)的復(fù)雜度等級給出重構(gòu)建議:
圈復(fù)雜度 | 代碼狀況 | 可測性 | 維護成本 |
---|---|---|---|
1 - 10 | 清晰、結(jié)構(gòu)化 | 高 | 低 |
10 - 20 | 復(fù)雜 | 中 | 中 |
20 - 30 | 非常復(fù)雜 | 低 | 高 |
>30 | 不可讀 | 不可測 | 非常高 |
圈復(fù)雜度 | 代碼狀況 |
---|---|
1 - 10 | 無需重構(gòu) |
11 - 15 | 建議重構(gòu) |
>15 | 強烈建議重構(gòu) |
6.架構(gòu)設(shè)計
將代碼復(fù)雜度檢測封裝成基礎(chǔ)包,根據(jù)自定義配置輸出檢測數(shù)據(jù),供其他應(yīng)用調(diào)用。
上面的展示了使用?eslint
?獲取代碼復(fù)雜度的思路,下面我們要把它封裝為一個通用的工具,考慮到工具可能在不同場景下使用,例如:網(wǎng)頁版的分析報告、cli版的命令行工具,我們把通用的能力抽象出來以?npm包
?的形式供其他應(yīng)用使用。
在計算項目代碼復(fù)雜度之前,我們首先要具備一項基礎(chǔ)能力,代碼掃描,即我們要知道我們要對項目里的哪些文件做分析,首先?eslint
?是具備這樣的能力的,我們也可以直接用?glob
?來遍歷文件。但是他們都有一個缺點,就是?ignore
?規(guī)則是不同的,這對于用戶來講是有一定學(xué)習(xí)成本的,因此我這里把手動封裝代碼掃描,使用通用的?npm ignore
?規(guī)則,這樣代碼掃描就可以直接使用?.gitignore
這樣的配置文件。另外,代碼掃描作為代碼分析的基礎(chǔ)能力,其他代碼分析也是可以公用的。
- 基礎(chǔ)能力
- 代碼掃描能力
- 復(fù)雜度檢測能力
- …
- 應(yīng)用
- 命令行工具
- 代碼分析報告
- …
7. 基礎(chǔ)能力 - 代碼掃描
本文涉及的?npm
?包和?cli
命令源碼均可在我的開源項目?awesome-cli中查看。
awesome-cli 是我新建的一個開源項目:有趣又實用的命令行工具,后面會持續(xù)維護,敬請關(guān)注,歡迎 star。
代碼掃描(c-scan
)源碼:awesome-cli/conard at master · ConardLi/awesome-cli · GitHub
代碼掃描是代碼分析的底層能力,它主要幫助我們拿到我們想要的文件路徑,應(yīng)該滿足我們以下兩個需求:
- 我要得到什么類型的文件
- 我不想要哪些文件
7.1 使用
1 2 3 4 5 6 7 8 9 10 | npm i c-scan --saveconst scan = require('c-scan'); scan({extensions:'**/*.js',rootPath:'src',defalutIgnore:'true',ignoreRules:[],ignoreFileName:'.gitignore' }); |
7.2 返回值
符合規(guī)則的文件路徑數(shù)組:
7.3 參數(shù)
-
extensions
- 掃描文件擴展名
- 默認(rèn)值:
**/*.js
-
rootPath
- 掃描文件路徑
- 默認(rèn)值:
.
-
defalutIgnore
- 是否開啟默認(rèn)忽略(
glob
規(guī)則) glob ignore
規(guī)則為內(nèi)部使用,為了統(tǒng)一ignore
規(guī)則,自定義規(guī)則使用gitignore
規(guī)則- 默認(rèn)值:
true
- 默認(rèn)開啟的?
glob ignore
?規(guī)則:
- 是否開啟默認(rèn)忽略(
1 2 3 4 5 6 7 | const DEFAULT_IGNORE_PATTERNS = ['node_modules/**','build/**','dist/**','output/**','common_build/**' ]; |
-
ignoreRules
- 自定義忽略規(guī)則(
gitignore
規(guī)則) - 默認(rèn)值:
[]
- 自定義忽略規(guī)則(
-
ignoreFileName
- 自定義忽略規(guī)則配置文件路徑(
gitignore
規(guī)則) - 默認(rèn)值:
.gitignore
- 指定為
null
則不啟用ignore
配置文件
- 自定義忽略規(guī)則配置文件路徑(
7.4 核心實現(xiàn)
基于?glob
?,自定義?ignore
?規(guī)則進行二次封裝。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | /*** 獲取glob掃描的文件列表* @param {*} rootPath 跟路徑* @param {*} extensions 擴展* @param {*} defalutIgnore 是否開啟默認(rèn)忽略*/ function getGlobScan(rootPath, extensions, defalutIgnore) {return new Promise(resolve => {glob(`${rootPath}${extensions}`,{ dot: true, ignore: defalutIgnore ? DEFAULT_IGNORE_PATTERNS : [] },(err, files) => {if (err) {console.log(err);process.exit(1);}resolve(files);});}); }/*** 加載ignore配置文件,并處理成數(shù)組* @param {*} ignoreFileName */ async function loadIgnorePatterns(ignoreFileName) {const ignorePath = path.resolve(process.cwd(), ignoreFileName);try {const ignores = fs.readFileSync(ignorePath, 'utf8');return ignores.split(/[\n\r]|\n\r/).filter(pattern => Boolean(pattern));} catch (e) {return [];} }/*** 根據(jù)ignore配置過濾文件列表* @param {*} files * @param {*} ignorePatterns * @param {*} cwd */ function filterFilesByIgnore(files, ignorePatterns, ignoreRules, cwd = process.cwd()) {const ig = ignore().add([...ignorePatterns, ...ignoreRules]);const filtered = files.map(raw => (path.isAbsolute(raw) ? raw : path.resolve(cwd, raw))).map(raw => path.relative(cwd, raw)).filter(filePath => !ig.ignores(filePath)).map(raw => path.resolve(cwd, raw));return filtered; } |
8. 基礎(chǔ)能力 - 代碼復(fù)雜度檢測
代碼復(fù)雜度檢測(c-complexity
)源碼:awesome-cli/code-complexity at master · ConardLi/awesome-cli · GitHub
代碼檢測基礎(chǔ)包應(yīng)該具備以下幾個能力:
- 自定義掃描文件夾和類型
- 支持忽略文件
- 定義最小提醒代碼復(fù)雜度
8.1 使用
1 2 3 4 | npm i c-complexity --saveconst cc = require('c-complexity'); cc({},10); |
8.2 返回值
- fileCount:文件數(shù)量
- funcCount:函數(shù)數(shù)量
- result:詳細(xì)結(jié)果
- funcType:函數(shù)類型
- funcName;函數(shù)名稱
- position:詳細(xì)位置(行列號)
- fileName:文件相對路徑
- complexity:代碼復(fù)雜度
- advice:重構(gòu)建議
8.3 參數(shù)
scanParam
- 繼承自上面代碼掃描的參數(shù)
min
- 最小提醒代碼復(fù)雜度,默認(rèn)為1
9. 應(yīng)用 - 代碼復(fù)雜度檢測工具
代碼復(fù)雜度檢測(conard cc
)源碼:awesome-cli/conard at master · ConardLi/awesome-cli · GitHub
9.1 指定最小提醒復(fù)雜度
可以觸發(fā)提醒的最小復(fù)雜度。
- 默認(rèn)為?
10
- 通過命令?
conard cc --min=5
?自定義
9.2 指定掃描參數(shù)
自定義掃描規(guī)則
- 掃描參數(shù)繼承自上面的?
scan param
- 例如:?
conard cc --defalutIgnore=false
10. 應(yīng)用 - 代碼復(fù)雜度報告
部分截圖來源于我們內(nèi)部的項目質(zhì)量監(jiān)控平臺,圈復(fù)雜度作為一項重要的指標(biāo),對于衡量項目代碼質(zhì)量起著至關(guān)重要的作用。
代碼復(fù)雜復(fù)雜度變化趨勢
定時任務(wù)爬取代碼每日的代碼復(fù)雜度、代碼行數(shù)、函數(shù)個數(shù),通過每日數(shù)據(jù)繪制代碼復(fù)雜度和代碼行數(shù)變化趨勢折線圖。
通過 [ 復(fù)雜度 / 代碼行數(shù) ] 或 [ 復(fù)雜度 / 函數(shù)個數(shù) ] 的變化趨勢,判斷項目發(fā)展是否健康。
-
比值若一直在上漲,說明你的代碼在變得越來越難以理解。這不僅使我們面臨意外的功能交互和缺陷的風(fēng)險,由于我們在具有或多或少相關(guān)功能的模塊中所面臨的過多認(rèn)知負(fù)擔(dān),也很難重用代碼并進行修改和測試。(下圖1)
-
若比值在某個階段發(fā)生突變,說明這段期間迭代質(zhì)量很差。(下圖2)
-
復(fù)雜度曲線圖可以很快的幫你更早的發(fā)現(xiàn)上面這兩個問題,發(fā)現(xiàn)它們后,你可能需要重構(gòu)代碼。復(fù)雜性趨勢對于跟蹤你的代碼重構(gòu)也很有用。復(fù)雜性趨勢的下降趨勢是一個好兆頭。這要么意味著您的代碼變得更簡單(例如,把 if-else 被重構(gòu)為多態(tài)解決方案),要么代碼更少(將不相關(guān)的部分提取到了其他模塊中)。(下圖3)
-
代碼重構(gòu)后,你還需要繼續(xù)探索復(fù)雜度變化趨勢。經(jīng)常發(fā)生的事情是,我們花費大量的時間和精力來重構(gòu),無法解決根本原因,很快復(fù)雜度又滑回了原處。(下圖4)你可能覺得這是個例,但是有研究標(biāo)明,在分析了數(shù)百個代碼庫后,發(fā)現(xiàn)出現(xiàn)這種情況的頻率很高。因此,時刻觀察代碼復(fù)雜度變化趨勢是有必要的。
代碼復(fù)雜度文件分布
統(tǒng)計各復(fù)雜度分布的函數(shù)數(shù)量。
代碼復(fù)雜度文件詳情
計算每個函數(shù)的代碼復(fù)雜度,從高到低依次列出高復(fù)雜度的文件分布,并給出重構(gòu)建議。
實際開發(fā)中并不一定所有的代碼都需要被分析,例如打包產(chǎn)物、靜態(tài)資源文件等等,這些文件往往會誤導(dǎo)我們的分析結(jié)果,現(xiàn)在分析工具會默認(rèn)忽略一些規(guī)則,例如:.gitignore文件、static目錄等等,實際這些規(guī)則還需要根據(jù)實際項目的情況去不斷完善,使分析結(jié)果變得更準(zhǔn)確。
參考
- 加推研發(fā)質(zhì)量與規(guī)范實戰(zhàn)
- codescene
- 圈復(fù)雜度那些事兒-前端代碼質(zhì)量系列文章(二)
- 代碼質(zhì)量管控 – 復(fù)雜度檢測
- 詳解圈復(fù)雜度