簡述
Cognitive Complexity:翻譯成中文是認(rèn)知復(fù)雜度,它將一段代碼被閱讀和理解時的復(fù)雜程度,估算成一個具體數(shù)字。一個方法的認(rèn)知復(fù)雜度基于以下三條簡單規(guī)則:
- 代碼中用到一些語法糖,把多句話縮為一句:代碼不會變得更復(fù)雜;
- 出現(xiàn)"break"中止了線性的代碼閱讀理解,如出現(xiàn)循環(huán)、條件、try-catch、switch-case、一串的and or操作符、遞歸,以及jump to label:代碼因此更復(fù)雜;
- 多層嵌套結(jié)構(gòu):代碼因此更復(fù)雜;
sonar官方論文翻譯
1. Abstract 概要
最初普遍使用圈復(fù)雜度(Cyclomatic Complexity)來描述一段代碼邏輯的“可測性與可維護(hù)性”,盡管用它來描述“可測性”很好(可測性在這里指:需要構(gòu)建出完善的單元測試需要多少代價),但它的模型設(shè)計,使其難以得出一個好的“可維護(hù)性”的測量結(jié)果。
本篇白皮書將描述一個新的度量標(biāo)準(zhǔn),彌補了“圈復(fù)雜度”的缺點,不再使用數(shù)學(xué)模型來評估。這這種新的度量,能夠更準(zhǔn)確地反映理解成本,以及維護(hù)這些代碼的困難程度。
盡管“認(rèn)知復(fù)雜性”與編程語言無關(guān),它同樣適用于其它語言的文件、類、過程與函數(shù)等等,但為方便起見,本篇僅使用面向?qū)ο螅ㄈ鏙ava)的術(shù)語:“類”與“方法”
2. Introduction 介紹
Thomas J. McCabe設(shè)計的圈復(fù)雜度是一個實際上的的標(biāo)準(zhǔn),用來測量一個模塊的控制流的復(fù)雜度。圈復(fù)雜度最初的目的是用來識別“難以測試和維護(hù)的軟件模塊”,它能算出最少的全覆蓋的測試用例量,但是不能測出一個讓人滿意的“理解難度”。這是因為同樣圈復(fù)雜度的代碼,不一定會導(dǎo)致相同的維護(hù)難度,這會導(dǎo)致我們對一些模塊有錯誤估計。
圈復(fù)雜度的理論是在1976年,于Fortran語言環(huán)境下設(shè)計的,因此如今使用它來衡量不再是那么全面了——它不包含一些現(xiàn)代的語言結(jié)構(gòu),如try-catch與lambda。
以及,每個方法都的最小圈復(fù)雜度都是1。這就讓我們無從得知,一個給定的類如果圈復(fù)雜度很高,它是一個大的、易維護(hù)的類,還是一個小的很復(fù)雜的邏輯。
不考慮單個類(考慮整個應(yīng)用),普遍認(rèn)為“圈復(fù)雜度”分?jǐn)?shù)與其代碼總和相關(guān)。因此看單個方法時,“圈復(fù)雜性”不夠有用。
為了解決這些問題,我們制定了認(rèn)知復(fù)雜度(Cognitive Complexity)以解決現(xiàn)代語言結(jié)構(gòu),并產(chǎn)生在類和應(yīng)用程序級別都有意義的一個度量值。 更重要的是,它不基于這一套數(shù)學(xué)模型,就可以對控制流進(jìn)行評估,這個值與程序員理解這些代碼片段所需的直覺,或理解難度相對應(yīng)。
3. An illustration of the problem 一個例子
這里給出一個很有用的例子,可以指出圈復(fù)雜度的問題。以下兩段方法有著相同的圈復(fù)雜度,但是在理解難度上差非常多:

圈復(fù)雜度理論,對上圖的兩個方法給出等同的復(fù)雜度,然而從直覺上顯然左邊的
sumOfPrimes要更難以理解一些。這也是為什么認(rèn)知復(fù)雜度舍棄了使用數(shù)學(xué)模型來評估一段邏輯,改用一組簡單的規(guī)則,把代碼的直覺理解程度轉(zhuǎn)為一個數(shù)字表達(dá)。
4. Basic criteria and methodology 基本原則
認(rèn)知復(fù)雜度的評估分?jǐn)?shù),是基下面三條基本規(guī)則:
- 忽略簡寫:把多句代碼縮寫為一句可讀的代碼,不改變理解難度;
- 對線性的代碼邏輯中,出現(xiàn)一個打斷邏輯的東西,難度+1;
- 當(dāng)打斷邏輯的是一個嵌套時,難度+1;
進(jìn)一步說,復(fù)雜度得分是來源于以下幾種不同的類型:(PS:這段比較抽象,后面有詳解)
A. Nesting:把一段代碼邏輯嵌套在另一段邏輯中;
B. Structural:被嵌套的控制流結(jié)構(gòu);
C. Fundamental:不受嵌套影響的語句;
D. Hybrid:一些控制流結(jié)構(gòu),但不包含在嵌套中;
然而不同類型在數(shù)學(xué)上沒有區(qū)別,都只是對復(fù)雜度加一。在要計算的不同類別之間進(jìn)行區(qū)分,可以更輕松地了解某一處是否適用嵌套的邏輯。以下各節(jié)將進(jìn)一步詳細(xì)說明這些規(guī)則及其背后的原理。
5. Ignore shorthand 忽略簡寫
在認(rèn)知復(fù)雜度的制定想法中,一個指導(dǎo)性的原則是:激勵使用者寫出好的編碼規(guī)范。也就是說,需要無視或低估讓代碼更可讀的feature(不計算進(jìn)復(fù)雜度)。
“方法”本身就是一個樸素的例子,把一段代碼拆的把幾句抽離成一個方法,用一句方法調(diào)用代替掉,“簡寫”它,認(rèn)知復(fù)雜度不會因為這這一次方法調(diào)用增加。
同樣的,認(rèn)知復(fù)雜度也會忽略掉null-coalescing操作符,x?.myObject這樣的操作符不增加復(fù)雜度,因為這些操作同樣是把多段代碼縮寫為一項了。例如下面的兩段代碼:

左側(cè)版本的代碼需要花了一小些時間來理解,而如果你理解null-coalescing的語法,右側(cè)版本立即能夠立即看明白。出于這樣的原因,計算認(rèn)知復(fù)雜度時會忽略掉null-coalescing操作。
6. Increment for breaks in the linear flow 打斷線性代碼流程導(dǎo)致的復(fù)雜
在認(rèn)知復(fù)雜度的制定想法中,另一項指導(dǎo)原則是:結(jié)構(gòu)控制會打斷一條線性的流從頭到尾走完,使代碼的維護(hù)者需要花更大功夫來理解代碼。在認(rèn)定了這會導(dǎo)致額外負(fù)擔(dān)的前提下,認(rèn)知復(fù)雜度評估了以下幾種會增加Structural類復(fù)雜度:
- 循環(huán): for, while, do while, ...
- 條件: 三元運算符, if, #if, #ifdef...
另外,以下這種會計處Hybrid類復(fù)雜度: - else if, elif, else, ...
但不計入Nesting類復(fù)雜度,因為這個量在計算之前的if語句時已經(jīng)計過了。
這些增加復(fù)雜度,其實和圈復(fù)雜度的計算方式非常像,但是額外的,認(rèn)知復(fù)雜度還會計算:
6.1 Catches
一個catch表達(dá)了控制流的一個分支,就像if一樣。因此每個catch語句都會增加Structural類的認(rèn)知復(fù)雜度,僅加1分,無論它catch住多少種異常。(在我們的計算中try\finally被直接忽略掉)
6.2 Switches
一個switch語句,和它附帶的全部case綁在一起記為一個Structural類,來增加復(fù)雜度。
在圈復(fù)雜度下,一個switch語句被視為一系列的if-else鏈,因此每個case都會增加復(fù)雜度,因為會使得控制流分支增多。
但以代碼維護(hù)的視角來看,一個switch:將單個變量與一組顯式的值比較,要比if-else鏈易于理解,因為if-else鏈可以用任意條件做比較。就是說if-else if鏈必須仔細(xì)的逐個閱讀條件,而switch通常是可以一目了然的。
6.3 Sequences of logical operators 一系列的邏輯操作
出于類似的原因,認(rèn)知復(fù)雜度不對每一個邏輯運算符計分,而是考慮對連續(xù)的一組邏輯操作加分。例如下面幾個操作:
a && b
a && b && c && d
a || b
a || b || c || d
理解后一行的操作,不會比理解前一行的操作更難。但是對于下面兩行,理解難度有質(zhì)的區(qū)別:
a && b && c && d
a || b && c || d
因為boolean操作表達(dá)式混合使用時會更難理解,因此對這類操作每出現(xiàn)一個,認(rèn)知復(fù)雜度都會不斷遞增。例如:

盡管認(rèn)知復(fù)雜度相對于循環(huán)復(fù)雜度,為類似的運算符提供了“折扣”,但它可以為所有的布爾運算符都有所增加。(例如那些變量賦值,方法調(diào)用和返回語句)
6.4 Recursion 遞歸
與圈復(fù)雜度不同,認(rèn)知復(fù)雜度對每一個遞歸調(diào)用,都增加一點Fundamental類復(fù)雜計分,不論是直接還是間接的。有兩個這樣做的動機(jī):
一、遞歸表達(dá)了一種“元循環(huán)”,并且循環(huán)會增加認(rèn)知復(fù)雜度;
二、認(rèn)知復(fù)雜度希望能用于估計一個方法,其控制流難以理解的程度,而即使是一些有經(jīng)驗的程序員,都覺得遞歸難以理解;
6.5 Jumps to labels
goto, break與continue到某處label,會增加Fundamental類復(fù)雜程度。但是在代碼過程中提前return,可以使代碼更清晰,所以其它類型的continue\break\return都不會導(dǎo)致復(fù)雜程度增加。
7. Increment for nested flow-break structures 嵌套打斷思路造成的復(fù)雜
直覺看起來很明顯,由連續(xù)嵌套的五個結(jié)構(gòu)比,線性連續(xù)的五個if\for結(jié)構(gòu)要好理解得多(不考慮執(zhí)行路徑上的第一個部分有幾句代碼)。因為這樣的嵌套會增加理解代碼的成本,所以認(rèn)知復(fù)雜度在計算時會將其視為一個Nesting類復(fù)雜度增加。
特別地,每一次有一個導(dǎo)致了Structural類或Hybrid類復(fù)雜的結(jié)構(gòu)體,嵌套了另一個結(jié)構(gòu)時,每一層嵌套都要再加一次Nesting類復(fù)雜度。例如下面的例子,這個方法本身和try這兩項就不會計入Nesting類的復(fù)雜,因為它們即不是Structure類也不是Hybrid類的復(fù)雜結(jié)構(gòu):

然而,對于if\for\while\catch這些結(jié)構(gòu),全部被視為Structural類和Nesting類的復(fù)雜。
此外,雖然最外層的方法被忽略了,并且lambda、#ifdef等類似功能也都不會視為Structral類的增量,但是它們會計入嵌套的層級數(shù):

8. The implications 含義
認(rèn)知復(fù)雜度制定的主要目標(biāo),是為方法計算出一個得分,準(zhǔn)確地反應(yīng)出此方法的相對理解難度。它的次要目標(biāo),是解決現(xiàn)代語言結(jié)構(gòu)的問題,并產(chǎn)生在方法級別以上也有價值的指標(biāo)。 可以證明,解決現(xiàn)代語言結(jié)構(gòu)的目標(biāo)已經(jīng)實現(xiàn)。 其他兩個目標(biāo)在下面進(jìn)行了檢查。
8.1 Intuitively ‘right’ complexity scores 直覺上對的復(fù)雜分
在本篇開頭的時候討論了兩個圈復(fù)雜度相同的方法(但它們有著完全不同的理解難度),現(xiàn)在回過頭來檢查一下這兩個方法的認(rèn)知復(fù)雜度吧:

認(rèn)知復(fù)雜度算法,給這兩個方法完全不同的得分,這個得分結(jié)果更接近它們的相對理解成本。
8.2 Metrics that are valuable above the method level 方法級別之上也有用的指標(biāo)
更進(jìn)一步的,因為認(rèn)知復(fù)雜度不會因為方法這個結(jié)構(gòu)增加,復(fù)雜度的總和開始有用了起來。現(xiàn)在你可以看出兩個類:一個有大量的getter()\setter()方法,另一個類僅有一個極其復(fù)雜的控制流,可以簡單的通過比較二者的認(rèn)知復(fù)雜度就行了。認(rèn)知復(fù)雜度可以成為衡量一個類或應(yīng)用的相對復(fù)雜度的工具。
9. Conclusion 結(jié)論
編寫和維護(hù)代碼是一個人為過程,它們的輸出必須遵守數(shù)學(xué)模型,但它們本身不適合數(shù)學(xué)模型。 這就是為什么數(shù)學(xué)模型不足以評估其所需的工作量的原因。
認(rèn)知復(fù)雜性不同于使用數(shù)學(xué)模型評估軟件可維護(hù)性的實踐。 它從圈復(fù)雜度設(shè)定的先例開始,但是使用人工判斷來評估應(yīng)如何對結(jié)構(gòu)進(jìn)行計數(shù),并決定應(yīng)向模型整體添加哪些內(nèi)容。 結(jié)果,它得出的方法復(fù)雜性得分比以前的模型更能吸引程序員,因為它們是對可理解性的更公平的相對評估。 此外,由于認(rèn)知復(fù)雜性不收取任何方法的“入門成本”,因此它不僅在方法級別,而且在類和服務(wù)級別,都產(chǎn)生了更加準(zhǔn)確的評估結(jié)果。
示例
