淺析程式碼圈複雜度及認知複雜度

g發表於2022-01-25

寫在開始

圈複雜度用來描述一段程式碼“可測性”很好(可測性這裡指需要構建完善的覆蓋全面的單元測試需要付出多少代價),但它的設計模型很難得出一個很好的“可讀性&可維護性”的測量結果

新版soanrqube引入了認知複雜度的概念,這個複雜度指標彌補了圈複雜度的一些不足,能更準確的反映一段程式碼的理解成本,以及維護這段程式碼的困難程度。

下面就簡要的描述下,為何認知複雜度更適合用來評價一段程式碼的可讀性及可維護性。

什麼是圈複雜度?

圈複雜度(Cyclomatic complexity)是一種程式碼複雜度的衡量標準,在1976年由Thomas J. McCabe, Sr. 提出,目標是為了指導程式設計師寫出更具可測性和可維護性的程式碼。

它可以用來衡量一個模組判定結構的複雜程度,數量上表現為獨立路徑條數,也可以理解為覆蓋所有可能的情況最少需要的測試用例數量。 

程式碼圈複雜度的計算方法

通常採用的計算方法為點邊計演算法(當然還有節點判定法),計算公式為:

V(G) = e – n + 2 

e 代表在控制流圖中的邊的數量(對應程式碼中順序結構的部分),n 代表在控制流圖中的節點數量,包括起點和終點(注:所有終點只計算一次,即便有多個return或者throw;節點對應程式碼中的分支語句)

假定有如下這樣一段程式碼:

圈複雜度計算方法

根據公式 V(G) = e – n + 2 = 12 – 8 + 2 = 6 ,上圖的圈複雜段為6。

注:說明一下為什麼n = 8,雖然圖上的真正節點有12個,但是其中有5個節點為throw、return,這樣的節點為end節點,只能記做一個

為什麼要引入認知複雜度?

圈複雜度最初的目的是用來識別“難以測試和維護的軟體模組”,它能算出最少的全覆蓋的測試用例量,但是不能測出一個讓人滿意的“理解難度”。

這是因為同樣圈複雜度的程式碼,不一定會具有相同的可維護性,我們看看下面的兩個例子:

上面這兩段程式碼具有相同的圈複雜度,但顯然不具有相同的可讀性和可維護性性,這就是圈複雜度的不足之處。

因為圈複雜度理論是在1976年提出的,它不包含一些現代的語言結構,比如try-catch、lambda。

並且,每個方法都預設有一個最小圈複雜度1,這就讓我們無從得知,一個給定的類如果圈複雜度很高,它是一個大的易維護的類,還是一個很小很複雜的類。

為了解決上述這些問題,所以引入了“認知複雜度”,它將一段程式碼被閱讀和理解時的複雜程度,估算成一個具體數字

認知複雜度如何評判?

認知複雜度評定基本原則

  • 對線性的程式碼邏輯中,出現一個打斷邏輯的東西,複雜度+1;
  • 當打斷邏輯的是一個巢狀時,複雜度+1;
  • 忽略簡寫:把多句程式碼縮寫為一句可讀的程式碼,複雜度不會額外增加;

上面這種描述可能有點抽象,具體一點說,以下控制流結構會導致認知複雜度增加:

for, while, do while, 三元運算子, if/elif/else, catch語句, 跳轉語句(goto/break/continue), 以及巢狀的控制流(每一層巢狀複雜度遞增)

我們繼續拿上面提到的兩個例子舉例:

圈複雜度對於getWord方法本身會預設有1的複雜度,每多一個case複雜度+1,所以最終圈複雜度為4

而認知複雜度,對於整個 switch 結構只增加1的複雜度,因為從可理解、可維護程度來說,多幾個case並不會導致其增加(當然,大量的case也是我們應當盡力去避免的)

我們接著看另外一個例子:

如你所看到的,認知複雜度考慮到了使這個方法比前面提到的getWords()方法更難理解的因素——巢狀以及跳轉語句

因此,雖然這兩個方法的圈複雜度是一樣的,但是它們的認知複雜度資料很好的反映了它們兩者在可理解性/可維護性上的差異。

另外,相對於圈複雜度預設所有方法至少有1的複雜度,認知複雜度並沒有這樣一個評定規則,這對於entity等簡單類的複雜度評判會更加友好和客觀:

綜上所述,認知複雜度作為程式碼的“可讀性/可維護性”評定指標會更加合適。


附、程式碼複雜度與軟體質量關係

以上覆雜度數值可以理解為方法粒度,即如果某一個方法複雜度>30,那這個方法的可讀性和可維護性就很低了

相關文章