1. 程式人生 > 實用技巧 >[轉]降低程式碼的圈複雜度——複雜程式碼的解決之道

[轉]降低程式碼的圈複雜度——複雜程式碼的解決之道

原文:https://www.cnblogs.com/detectiveHLH/p/14206712.html

------------------------------------------------

0. 什麼是圈複雜度

可能你之前沒有聽說過這個詞,也會好奇這是個什麼東西是用來幹嘛的,在維基百科上有這樣的解釋。

Cyclomatic complexityis a software metric used to indicate the complexity of a program. It is a quantitative measure of the number of linearly independent paths through a program's source code. It was developed byThomas

J. McCabe, Sr. in1976.

簡單翻譯一下就是,圈複雜度是用來衡量程式碼複雜程度的,圈複雜度的概念是由這哥們Thomas J. McCabe, Sr在1976年的時候提出的概念。

1. 為什麼需要圈複雜度

如果你現在的專案,程式碼的可讀性非常差,難以維護,單個函式程式碼特別的長,各種if else case巢狀,看著大段大段寫的糟糕的程式碼無從下手,甚至到了根本看不懂的地步,那麼你可以考慮使用圈複雜度來衡量自己專案中程式碼的複雜性。

如果不刻意的加以控制,當我們的專案達到了一定的規模之後,某些較為複雜的業務邏輯就會導致有些開發寫出很複雜的程式碼。

舉個真實的複雜業務的例子,如果你使用TDD

Test-DrivenDevelopment)的方式進行開發的話,當你還沒有真正開始寫某個介面的實現的時候,你寫的單測可能都已經達到了好幾十個case,而真正的業務邏輯甚至還沒有開始寫

再例如,一個函式,有幾百、甚至上千行的程式碼,除此之外各種if else while巢狀,就算是寫程式碼的人,可能過幾周忘了上下文再來看這個程式碼,可能也看不懂了,因為其程式碼的可讀性太差了,你讀懂都很困難,又談什麼維護性和可擴充套件性呢?

那我們如何在編碼中,CR(Code Review)中提早的避免這種情況呢?使用圈複雜度的檢測工具,檢測提交的程式碼中的圈複雜度的情況,然後根據圈複雜度檢測情況進行重構。把過長過於複雜的程式碼拆成更小的、職責單一且清晰的函式,或者是用設計模式

來解決程式碼中大量的if else的巢狀邏輯。

可能有的人會認為,降低圈複雜度對我收益不怎麼大,可能從短期上來看是這樣的,甚至你還會因為動了其他人的程式碼,觸發了圈複雜度的檢測,從而還需要去重構別人寫的程式碼。

但是從長期看,低圈複雜度的程式碼具有更佳的可讀性、擴充套件性和可維護性。同時你的編碼能力隨著設計模式的實戰運用也會得到相應的提升。

2. 圈複雜度度量標準

那圈複雜度,是如何衡量程式碼的複雜程度的?不是憑感覺,而是有著自己的一套計算規則。有兩種計算方式,如下:

  1. 節點判定法
  2. 點邊計演算法

判定標準我整理成了一張表格,僅供參考。

圈複雜度說明
1 - 10 程式碼是OK的,質量還行
11 - 15 程式碼已經較為複雜,但也還好,可以設法對某些點重構一下
16 - ∞ 程式碼已經非常的複雜了,可維護性很低, 維護的成本也大,此時必須要進行重構

當然,我個人認為不能夠武斷的把這個圈複雜度的標準應用於所有公司的所有情況,要按照自己的實際情況來分析。

這個完全是看自己的業務體量和實際情況來決定的。假設你的業務很簡單,而且是個單體應用,功能都是很簡單的CRUD,那你的圈複雜度即使想上去也沒有那麼容易。此時你就可以選擇把圈複雜度的重構閾值設定為10.

而假設你的業務十分複雜,而且涉及到多個其他的微服務系統呼叫,再加上各種業務中的corner case的判斷,圈複雜度上100可能都不在話下。

而這樣的程式碼,如果不進行重構,後期隨著需求的增加,會越壘越多,越來越難以維護。

2.1 節點判定法

這裡只介紹最簡單的一種,節點判定法,因為包括有的工具其實也是按照這個演算法去演算法的,其計算的公式如下。

圈複雜度 = 節點數量 + 1

節點數量代表什麼呢?就是下面這些控制節點。

if、for、while、case、catch、與、非、布林操作、三元運算子

大白話來說,就是看到上面符號,就把圈複雜度加1,那麼我們來看一個例子。

我們按照上面的方法,可以得出節點數量是13,那麼最終的圈複雜度就等於13 + 1 = 14,圈複雜度是14,值得注意的是,其中的&&也會被算作節點之一。

2.2 使用工具

對於golang我們可以使用gocognit來判定圈複雜度,你可以使用go get github.com/uudashr/gocognit/cmd/gocognit快速的安裝。然後使用gocognit $file就可以判斷了。我們可以新建檔案test.go

packagemain

import(
"flag"
"log"
"os"
"sort"
)

funcmain(){
log.SetFlags(0)
log.SetPrefix("cognitive:")
flag.Usage=usage
flag.Parse()
args:=flag.Args()
iflen(args)==0{
usage()
}

stats:=analyze(args)
sort.Sort(byComplexity(stats))
written:=writeStats(os.Stdout,stats)

if*avg{
showAverage(stats)
}

if*over>0&&written>0{
os.Exit(1)
}
}

然後使用命令gocognit test.go,來計算該程式碼的圈複雜度。

$gocognittest.go
6mainmaintest.go:11:1

表示main包的main方法從11行開始,其計算出的圈複雜度是6

3. 如何降低圈複雜度

這裡其實有很多很多方法,然後各類方法也有很多專業的名字,但是對於初瞭解圈複雜度的人來說可能不是那麼好理解。所以我把如何降低圈複雜度的方法總結成了一句話那就是——“儘量減少節點判定法中節點的數量”。

換成大白話來說就是,儘量少寫if、else、while、case這些流程控制語句。

其實你在降低你原本程式碼的圈複雜度的時候,其實也算是一種重構。對於大多數的業務程式碼來說,程式碼越少,對於後續維護閱讀程式碼的人來說就越容易理解。

簡單總結下來就兩個方向,一個是拆分小函式,另一個是想盡辦法少些流程控制語句。

3.1 拆分小函式

拆分小函式,圈複雜度的計算範圍是在一個function內的,將你的複雜的業務程式碼拆分成一個一個的職責單一的小函式,這樣後面閱讀的程式碼的人就可以一眼就看懂你大概在幹嘛,然後具體到每一個小函式,由於它職責單一,而且程式碼量少,你也很容易能夠看懂。除了能夠降低圈複雜度,拆分小函式也能夠提高程式碼的可讀性和可維護性。

比如程式碼中存在很多condition的判斷。

其實可以優化成我們單獨拆分一個判斷函式,只做condition判斷這一件事情。

3.2 少寫流程控制語句

這裡舉個特別簡單的例子。

其實可以直接優化成下面這個樣子。

例子就先舉到這裡,其實你也發現,其實就像我上面說的一樣,其目的就是為了減少if等流程控制語句。其實換個思路想,複雜的邏輯判斷肯定會增加我們閱讀程式碼的理解成本,而且不便於後期的維護。所以,重構的時候可以想辦法儘量去簡化你的程式碼。

那除了這些還有沒有什麼更加直接一點的方法呢?例如從一開始寫程式碼的時候就儘量去避免這個問題。