認知複雜度——估算項目代碼的理解成本

1. 摘要

Cognitive Complexity:認知複雜度,是由sonarQube設計的一個算法,算法將一段程序代碼被理解的複雜程度,估算成一個整數——可以等同於代碼的理解成本。
IntellliJ IDEA支持sonar插件,可以實時計算函數的認知複雜度,當複雜度過高時將提醒重構。

認知複雜度的計算基於以下三規則:語法糖不增加理解難度、打斷線性代碼執行增加理解難度、多層嵌套增加理解難度。

2. 歷史背景

(此部分可略過)

2.1 工程複雜度背景

最初工業界普遍使用圈複雜度(Cyclomatic Complexity)來描述一段代碼邏輯的“可測性與可維護性”,儘管用它來描述“可測性”很好(可測性的意思是:需要構建出完美的單元測試需要多少代價),但這種模型不能很好的描述代碼的“可維護性”。
認知複雜度很好的彌補了“圈複雜度”的這個缺點,用一種全新的、簡單的度量方法,準確地反映代碼的理解成本,以及維護的難度。
“認知複雜度”與編程語言無關,它可以用來測量文件、類、過程與函數等等概念下的複雜度。

2.2 有哪些問題待解決

  • Thomas J. McCabe設計的圈複雜度是一個業界標準,最初目的是用來識別“難以測試和維護的軟件模塊”——但是用這種算法只能算出一個模塊最少的全覆蓋測試用例數量,而不能算出一個精確的模塊“理解難度”。因此圈複雜度一樣的兩段代碼,維護難度卻有可能天差地別,用圈複雜度來理解維護難度,導致我們對模塊代碼有錯誤估計。
  • 圈複雜度的理論是在1976年於Fortran語言環境下設計的,如今使用它來衡量新語言不再是那麼全面了。一些現代的語言結構,如try-catch與lambda沒有被考慮在內。
  • 每個方法都的最小圈複雜度都是1,導致了“圈複雜度”與其方法數量是相關。一個很好理解的Class(類),也可能因爲包含多個簡單方法,總複雜度被擡得很高。
    爲了解決這些問題,SonarQube制定了認知複雜度(Cognitive Complexity),一方面解決了圈複雜度在現代語言結構的不足,一方面使複雜度在方法、類、應用程序級別都有實際意義。 更重要的是,這個複雜度值與程序員理解這些代碼片段所需的直覺(理解難度)相對應。

2.3 一個例子

這裏給出一個很有用的例子,可以指出圈複雜度的問題。以下兩段方法有着相同的圈複雜度,但是在理解難度上差非常多:
image.png
圈複雜度理論,對上圖的兩個方法給出等同的複雜度,然而從直覺上顯然左邊的sumOfPrimes要更難以理解一些。這也是爲什麼認知複雜度捨棄了使用數學模型來評估一段邏輯,改用一組簡單的規則,把代碼的直覺理解程度轉爲一個數字表達。

3. 認知複雜度解釋

3.1 基本原則

(3.1小節僅作爲規則提煉,細節解釋見3.2-3.6)

認知複雜度的評估分數,是基下面三條基本規則:

  1. 忽略簡寫:把多句代碼縮寫爲一句可讀的代碼(語法糖),不改變理解難度;
  2. 打斷線性的代碼邏輯:出現一個打斷邏輯線性執行的語句,難度+1;
  3. 當打斷邏輯的是一個嵌套時,難度+1;

以下四種不同類型,均會使認知複雜度得分加一:
A. Nesting:把一段代碼邏輯嵌套在另一段邏輯中;
B. Structural:被嵌套的控制流結構;
C. Fundamental:不受嵌套影響的語句;
D. Hybrid:一些控制流結構,但不包含在嵌套中;

3.2 忽略語法糖

認知複雜度的指導性的原則是:鼓勵使用者寫出好的編碼規範。因此讓代碼更可讀的feature,是不需要計入複雜度的。比如一次簡單的方法調用,又或者使用語法特性完成的一個可理解動作。
比如null-coalescing操作符x?.myObject,如下圖代碼),就是屬於語法糖,是不會增加認知複雜度的:
image.png
左側的代碼會因爲if語句打斷了邏輯,使得理解難度上升了,因此認知複雜度不爲0;
右側的代碼邏輯是可以直接理解的語法糖,因此複雜度爲0。

3.3 各種類型的認知複雜

認知複雜度的另一項指導原則:控制流會打斷一條線性的執行流——程序不再是一行行往下走了,因此代碼的維護者需要花更大功夫來理解它。

3.3.1 Structural類複雜度

一、循環: for, while, do while, …
二、條件: 三元運算符, if, #if, #ifdef…
三、Catch:
一個catch表達了控制流的一個分支,就像if一樣。因此每個catch語句都會增加Structural類的認知複雜度,僅加1分,無論它catch住多少種異常。(在我們的計算中try\finally被直接忽略掉)
四、Switch:
一個switch語句,和它附帶的全部case綁在一起記爲一個Structural類,複雜度僅增加1。(不像圈複雜度,每個case都會使得控制流分支增加,進而增加圈複雜度)
Switch可以視爲用單個變量與一組值作匹配,是一目瞭然的,因此要比if-else鏈更容易理解,因此認知複雜度更低。

3.3.2 Hybrid類複雜度

  • else if, elif
  • else …

計算爲Hybrid類複雜度以後,不會再計入Nesting類複雜度,因爲嵌套的複雜程度在if語句時候已經計入了。(見3.3.5)

3.3.3 一連串的邏輯操作

認知複雜度不對每一個邏輯運算符計分,而是考慮對連續的一組邏輯操作加分。例如下面幾個操作:

a && b
a && b && c && d
a || b
a || b || c || d

理解後一行的操作,不會比理解前一行的操作更難,上面的四種認知複雜度都是1。但是對於下面兩行,理解難度有質的區別:

a && b && c && d
a || b && c || d

這是因爲boolean操作表達式混合使用時,難度會顯著上升,因此認知複雜度的值會不斷遞增。具體計算方法如下:
image.png
儘管認知複雜度相對於循環複雜度,爲類似的運算符提供了“折扣”,但它可以爲所有的布爾運算符都有所增加。(例如那些變量賦值,方法調用和返回語句)

3.3.4 Fundamental類複雜度

以下幾種均爲Fundamental類,會將認知複雜度+1
一、遞歸
與普通的調用函數不同,每一個遞歸調用,都增加一點Fundamental類複雜度計分,不論是直接還是間接的。有兩個這樣做的動機:

  • 遞歸表達了一種“元循環”,並且循環會增加認知複雜度;
  • 認知複雜度希望能用於估計一個方法,其控制流難以理解的程度,而即使是一些有經驗的程序員,都覺得遞歸難以理解;
    二、Jump類語句
    goto, break與continue到某處,都會增加Fundamental類複雜程度。但是在代碼過程中提前return,可以使代碼更清晰,所以其它類型的continue\break\return都不會導致複雜程度增加。

3.3.5 Nesting複雜度

直覺上連續嵌套的代碼,要比線性代碼難理解很多。這樣的嵌套會增加理解代碼的成本,所以認知複雜度在計算時會將其單獨歸類,視爲一個Nesting類的複雜度增加。
當一個Structural類或Hybrid類的結構體,嵌套其它邏輯時,每一層嵌套都要額外計入一次Nesting類複雜度。這樣說有點抽象,可以結合下面的例子來理解:
image.png
這個方法(myMethod方法)與try這兩項,是不會計入Nesting類的複雜的,因爲它們即不是Structure類也不是Hybrid類的複雜結構:
然而,對於if\for\while\catch這些結構是Structural類,再與Nesting類結構結合,就會按遞增的增加複雜度(+1 +2… +n)。
此外,lambda、#ifdef等類似代碼雖然不是Structural類型,但是它們會增加嵌套的層數,如下例所示:
image.png

3.4 認知複雜度的目標

認知複雜度制定的主要目標,是爲方法計算出一個得分,準確地反應出此方法的相對理解難度。它的次要目標,是解決現代語言結構的問題,併產生在方法級別以上也有價值的指標。 可以證明,解決現代語言結構的目標已經實現。 其他兩個目標在下面進行了檢查。

3.5.1 認知複雜度的示例

在本篇開頭的時候討論了兩個圈複雜度相同的方法,它們可能有着完全不同的理解難度,那麼認知複雜度的表現如何呢?
image.png
上面兩個有着相同圈複雜度的代碼,卻有着完全不同的認知複雜度。左側得分爲7,右側則爲1,這個結果比較接近它們的真實理解成本。

3.5.2 方法之上的級別

因爲認知複雜度不會因爲方法這個結構增加,複雜度總和的指標變得更有意義。一個有大量的getter()\setter()方法組合的類,擁有較低的認知複雜度,另一個類僅一個極其複雜的控制流的方法,會擁有着較大的認知複雜度。
我們可以簡單的計算一個類、一個模塊、一個應用的認知複雜度總和,用這個總和就可以估計出總體理解成本。

3.6 Conclusion 結論

編寫和維護代碼是一個人爲過程,它們的輸出必須遵守數學模型,但它們本身不適合數學模型。 這就是爲什麼數學模型不足以評估其所需的工作量的原因。
認知複雜性不同於使用數學模型評估軟件可維護性的實踐。 它從圈複雜度設定的先例開始,但是使用人工判斷來評估應如何對結構進行計數,並決定應向模型整體添加哪些內容。 結果,它得出的方法複雜性得分比以前的模型更能吸引程序員,因爲它們是對可理解性的更公平的相對評估。 此外,由於認知複雜性不收取任何方法的“入門成本”,因此它不僅在方法級別,而且在類和服務級別,都產生了更加準確的評估結果。

4. 其它

4.1 計算認知複雜度的示例

image.png

4.2 引用

sonar官網 白皮書
code climate官網 條目解釋

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章