您好,登錄后才能下訂單哦!
本篇內容主要講解“怎么降低代碼的圈復雜度”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“怎么降低代碼的圈復雜度”吧!
0. 什么是圈復雜度
可能你之前沒有聽說過這個詞,也會好奇這是個什么東西是用來干嘛的,在維基百科上有這樣的解釋。
Cyclomatic complexity is 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 by Thomas J. McCabe, Sr. in 1976.
簡單翻譯一下就是,圈復雜度是用來衡量代碼復雜程度的,圈復雜度的概念是由這哥們Thomas J. McCabe, Sr在1976年的時候提出的概念。
1. 為什么需要圈復雜度
如果你現在的項目,代碼的可讀性非常差,難以維護,單個函數代碼特別的長,各種if else case嵌套,看著大段大段寫的糟糕的代碼無從下手,甚至到了根本看不懂的地步,那么你可以考慮使用圈復雜度來衡量自己項目中代碼的復雜性。
如果不刻意的加以控制,當我們的項目達到了一定的規模之后,某些較為復雜的業務邏輯就會導致有些開發寫出很復雜的代碼。
舉個真實的復雜業務的例子,如果你使用TDD(Test-Driven Development)的方式進行開發的話,當你還沒有真正開始寫某個接口的實現的時候,你寫的單測可能都已經達到了好幾十個case,而真正的業務邏輯甚至還沒有開始寫
再例如,一個函數,有幾百、甚至上千行的代碼,除此之外各種if else while嵌套,就算是寫代碼的人,可能過幾周忘了上下文再來看這個代碼,可能也看不懂了,因為其代碼的可讀性太差了,你讀懂都很困難,又談什么維護性和可擴展性呢?
那我們如何在編碼中,CR(Code Review)中提早的避免這種情況呢?使用圈復雜度的檢測工具,檢測提交的代碼中的圈復雜度的情況,然后根據圈復雜度檢測情況進行重構。把過長過于復雜的代碼拆成更小的、職責單一且清晰的函數,或者是用設計模式來解決代碼中大量的if else的嵌套邏輯。
可能有的人會認為,降低圈復雜度對我收益不怎么大,可能從短期上來看是這樣的,甚至你還會因為動了其他人的代碼,觸發了圈復雜度的檢測,從而還需要去重構別人寫的代碼。
但是從長期看,低圈復雜度的代碼具有更佳的可讀性、擴展性和可維護性。同時你的編碼能力隨著設計模式的實戰運用也會得到相應的提升。
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。
package main
import (
"flag"
"log"
"os"
"sort"
)
func main() {
log.SetFlags(0)
log.SetPrefix("cognitive: ")
flag.Usage = usage
flag.Parse()
args := flag.Args()
if len(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,來計算該代碼的圈復雜度。
$ gocognit test.go
6 main main test.go:11:1
表示main包的main方法從11行開始,其計算出的圈復雜度是6。
3. 如何降低圈復雜度
這里其實有很多很多方法,然后各類方法也有很多專業的名字,但是對于初了解圈復雜度的人來說可能不是那么好理解。所以我把如何降低圈復雜度的方法總結成了一句話那就是——“盡量減少節點判定法中節點的數量”。
換成大白話來說就是,盡量少寫if、else、while、case這些流程控制語句。
其實你在降低你原本代碼的圈復雜度的時候,其實也算是一種重構。對于大多數的業務代碼來說,代碼越少,對于后續維護閱讀代碼的人來說就越容易理解。
簡單總結下來就兩個方向,一個是拆分小函數,另一個是想盡辦法少些流程控制語句。
3.1 拆分小函數
拆分小函數,圈復雜度的計算范圍是在一個function內的,將你的復雜的業務代碼拆分成一個一個的職責單一的小函數,這樣后面閱讀的代碼的人就可以一眼就看懂你大概在干嘛,然后具體到每一個小函數,由于它職責單一,而且代碼量少,你也很容易能夠看懂。除了能夠降低圈復雜度,拆分小函數也能夠提高代碼的可讀性和可維護性。
比如代碼中存在很多condition的判斷。
其實可以優化成我們單獨拆分一個判斷函數,只做condition判斷這一件事情。
圖片
3.2 少寫流程控制語句
這里舉個特別簡單的例子。
圖片
其實可以直接優化成下面這個樣子。
圖片
例子就先舉到這里,其實你也發現,其實就像我上面說的一樣,其目的就是為了減少if等流程控制語句。其實換個思路想,復雜的邏輯判斷肯定會增加我們閱讀代碼的理解成本,而且不便于后期的維護。所以,重構的時候可以想辦法盡量去簡化你的代碼。
那除了這些還有沒有什么更加直接一點的方法呢?例如從一開始寫代碼的時候就盡量去避免這個問題。
4. 使用go-linq
我們先不用急著去了解go-linq是什么,我們先來看一個經典的業務場景問題。
從一個對象列表中獲取一個ID列表
如果在go中,我們可以這么做。
圖片
略顯繁瑣,熟悉Java的同學可能會說,這么簡單的功能為什么會寫的這么復雜,于是三下五除二寫下了如下的代碼。
圖片
上圖中使用了Java8的新特性Stream,而Go語言目前還無法達到這樣的效果。于是就該輪到go-linq出場了,使用go-linq之后的代碼就變成了如下的模樣。
圖片
怎么樣,是不是看到Java 8 Stream的影子,重構之后的代碼我們暫且不去比較行數,從語意上看,同樣的清晰直觀,這就是go-linq,我們用了一個例子來為大家介紹了它的定義,接下來簡單介紹幾種常見的用法,這些都是官網上給的例子。
4.1 ForEach
與Java 8中的foreach是類似的,就是對集合的一個遍歷。
圖片
首先是一個From,這代表了輸入,夢開始的地方,可以和Java 8中的stream劃等號。
然后可以看到有ForEach和ForEachT,ForEachIndexed和ForEachIndexedT。前者是只遍歷元素,后者則將其下標也一起打印了出來。跟Go中的Range是一樣的,跟Java 8的ForEach也類似,但是Java 8的ForEach沒有下標,之所以go-ling有,是因為它自己記錄了一個index,ForEachIndexed源碼如下。
圖片
其中兩者的區別是啥呢?我認識是你對你要遍歷的元素的類型是否敏感,其實大多數情況應該都是敏感的。如果你使用了帶T的,那么在遍歷的時候go-ling會將interface轉成你在函數中所定義的類型,例如fruit string。
否則的話,就需要我們自己去手動的將interface轉換成對應的類型,所以后續的所有的例子我都會直接使用ForEachT這種類型的函數。
4.2 Where
可以理解為SQL中的where條件,也可以理解為Java 8中的filter,按照某些條件對集合進行過濾。
圖片
上面的Where篩選出了字符串長度大于6的元素,可以看到其中有個ToSlice,就是將篩選后的結果輸出到指定的slice中。
4.3 Distinct
與你所了解到的MySQL中的Distinct,又或者是Java 8中的Distinct是一樣的作用,去重。
4.3.1 簡單場景
4.3.2 復雜場景
當然,實際的開發中,這種只有一個整形數組的情況是很少的,大部分需要判斷的對象都是一個struct數組。所以我們再來看一個稍微復雜一點的例子。
圖片
上面的代碼是對一個products的slice,根據product的Code字段來進行去重。
4.4 Except
對兩個集合做差集。
4.4.1 簡單場景圖片
4.4.2 復雜場景圖片
4.5 Intersect
對兩個集合求交集。
4.5.1 簡單場景圖片
4.5.2 復雜場景圖片
4.6 Select
從功能上來看,Select跟ForEach是差不多的,區別如下。
Select 返回了一個Query對象
ForEach 沒有返回值
在這里你不用去關心Query對象到底是什么,就跟Java8中的map、filter等等控制函數都會返回Stream一樣,通過返回Query,來達到代碼中流式編程的目的。
4.6.1 簡單場景
圖片
select簡單場景
其中SelectT就是遍歷了一個集合,然后做了一些運算,將運算之后的結果輸出到了新的slice中。
SelectMany為集合中的每一個元素都返回一個Query,跟Java 8中的flatMap類似,flatMap則是為每個元素創建一個Stream。簡單來說就是把一個二維數組給它拍平成一維數組。
4.6.2 復雜場景圖片
4.7 Group圖片
Group根據指定的元素對結合進行分組,Group`的源碼如下。
圖片
Key就是我們分組的時候用key,Group就是分組之后得到的對應key的元素列表。
好了,由于篇幅的原因,關于go-linq的使用就先介紹到這里,感興趣的可以去go-linq官網查看全部的用法。
5. 關于go-linq的使用
首先我認為使用go-linq不僅僅是為了“逃脫”檢測工具對圈復雜度的檢查,而是真正的通過重構自己的代碼,讓其變的可讀性更佳。
舉個例子,在某些復雜場景下,使用go-linq反而會讓你的代碼更加的難以理解。代碼是需要給你和后續維護的同學看的,不要盲目的去追求低圈復雜度的代碼,而瘋狂的使用go-linq。
我個人其實只傾向于使用go-linq對集合的一些操作,其他的復雜情況,好的代碼,加上適當的注釋,才是不給其他人(包括你自己)挖坑的行為。而且并不是說所有的if else都是爛代碼,如果必要的if else能夠大大增加代碼的可讀性,何樂而不為?(這里當然說的不是那種滿屏各種if else前套的代碼)
到此,相信大家對“怎么降低代碼的圈復雜度”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。