您好,登錄后才能下訂單哦!
本篇內(nèi)容主要講解“怎么降低代碼的圈復(fù)雜度”,感興趣的朋友不妨來(lái)看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來(lái)帶大家學(xué)習(xí)“怎么降低代碼的圈復(fù)雜度”吧!
0. 什么是圈復(fù)雜度
可能你之前沒(méi)有聽(tīng)說(shuō)過(guò)這個(gè)詞,也會(huì)好奇這是個(gè)什么東西是用來(lái)干嘛的,在維基百科上有這樣的解釋。
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.
簡(jiǎn)單翻譯一下就是,圈復(fù)雜度是用來(lái)衡量代碼復(fù)雜程度的,圈復(fù)雜度的概念是由這哥們Thomas J. McCabe, Sr在1976年的時(shí)候提出的概念。
1. 為什么需要圈復(fù)雜度
如果你現(xiàn)在的項(xiàng)目,代碼的可讀性非常差,難以維護(hù),單個(gè)函數(shù)代碼特別的長(zhǎng),各種if else case嵌套,看著大段大段寫(xiě)的糟糕的代碼無(wú)從下手,甚至到了根本看不懂的地步,那么你可以考慮使用圈復(fù)雜度來(lái)衡量自己項(xiàng)目中代碼的復(fù)雜性。
如果不刻意的加以控制,當(dāng)我們的項(xiàng)目達(dá)到了一定的規(guī)模之后,某些較為復(fù)雜的業(yè)務(wù)邏輯就會(huì)導(dǎo)致有些開(kāi)發(fā)寫(xiě)出很復(fù)雜的代碼。
舉個(gè)真實(shí)的復(fù)雜業(yè)務(wù)的例子,如果你使用TDD(Test-Driven Development)的方式進(jìn)行開(kāi)發(fā)的話,當(dāng)你還沒(méi)有真正開(kāi)始寫(xiě)某個(gè)接口的實(shí)現(xiàn)的時(shí)候,你寫(xiě)的單測(cè)可能都已經(jīng)達(dá)到了好幾十個(gè)case,而真正的業(yè)務(wù)邏輯甚至還沒(méi)有開(kāi)始寫(xiě)
再例如,一個(gè)函數(shù),有幾百、甚至上千行的代碼,除此之外各種if else while嵌套,就算是寫(xiě)代碼的人,可能過(guò)幾周忘了上下文再來(lái)看這個(gè)代碼,可能也看不懂了,因?yàn)槠浯a的可讀性太差了,你讀懂都很困難,又談什么維護(hù)性和可擴(kuò)展性呢?
那我們?nèi)绾卧诰幋a中,CR(Code Review)中提早的避免這種情況呢?使用圈復(fù)雜度的檢測(cè)工具,檢測(cè)提交的代碼中的圈復(fù)雜度的情況,然后根據(jù)圈復(fù)雜度檢測(cè)情況進(jìn)行重構(gòu)。把過(guò)長(zhǎng)過(guò)于復(fù)雜的代碼拆成更小的、職責(zé)單一且清晰的函數(shù),或者是用設(shè)計(jì)模式來(lái)解決代碼中大量的if else的嵌套邏輯。
可能有的人會(huì)認(rèn)為,降低圈復(fù)雜度對(duì)我收益不怎么大,可能從短期上來(lái)看是這樣的,甚至你還會(huì)因?yàn)閯?dòng)了其他人的代碼,觸發(fā)了圈復(fù)雜度的檢測(cè),從而還需要去重構(gòu)別人寫(xiě)的代碼。
但是從長(zhǎng)期看,低圈復(fù)雜度的代碼具有更佳的可讀性、擴(kuò)展性和可維護(hù)性。同時(shí)你的編碼能力隨著設(shè)計(jì)模式的實(shí)戰(zhàn)運(yùn)用也會(huì)得到相應(yīng)的提升。
2. 圈復(fù)雜度度量標(biāo)準(zhǔn)
那圈復(fù)雜度,是如何衡量代碼的復(fù)雜程度的?不是憑感覺(jué),而是有著自己的一套計(jì)算規(guī)則。有兩種計(jì)算方式,如下:
節(jié)點(diǎn)判定法
點(diǎn)邊計(jì)算法
判定標(biāo)準(zhǔn)我整理成了一張表格,僅供參考。
圈復(fù)雜度 說(shuō)明
1 - 10 代碼是OK的,質(zhì)量還行
11 - 15 代碼已經(jīng)較為復(fù)雜,但也還好,可以設(shè)法對(duì)某些點(diǎn)重構(gòu)一下
16 - ∞ 代碼已經(jīng)非常的復(fù)雜了,可維護(hù)性很低, 維護(hù)的成本也大,此時(shí)必須要進(jìn)行重構(gòu)
當(dāng)然,我個(gè)人認(rèn)為不能夠武斷的把這個(gè)圈復(fù)雜度的標(biāo)準(zhǔn)應(yīng)用于所有公司的所有情況,要按照自己的實(shí)際情況來(lái)分析。
這個(gè)完全是看自己的業(yè)務(wù)體量和實(shí)際情況來(lái)決定的。假設(shè)你的業(yè)務(wù)很簡(jiǎn)單,而且是個(gè)單體應(yīng)用,功能都是很簡(jiǎn)單的CRUD,那你的圈復(fù)雜度即使想上去也沒(méi)有那么容易。此時(shí)你就可以選擇把圈復(fù)雜度的重構(gòu)閾值設(shè)定為10.
而假設(shè)你的業(yè)務(wù)十分復(fù)雜,而且涉及到多個(gè)其他的微服務(wù)系統(tǒng)調(diào)用,再加上各種業(yè)務(wù)中的corner case的判斷,圈復(fù)雜度上100可能都不在話下。
而這樣的代碼,如果不進(jìn)行重構(gòu),后期隨著需求的增加,會(huì)越壘越多,越來(lái)越難以維護(hù)。
2.1 節(jié)點(diǎn)判定法
這里只介紹最簡(jiǎn)單的一種,節(jié)點(diǎn)判定法,因?yàn)榘ㄓ械墓ぞ咂鋵?shí)也是按照這個(gè)算法去算法的,其計(jì)算的公式如下。
圈復(fù)雜度 = 節(jié)點(diǎn)數(shù)量 + 1
節(jié)點(diǎn)數(shù)量代表什么呢?就是下面這些控制節(jié)點(diǎn)。
if、for、while、case、catch、與、非、布爾操作、三元運(yùn)算符
大白話來(lái)說(shuō),就是看到上面符號(hào),就把圈復(fù)雜度加1,那么我們來(lái)看一個(gè)例子。
圖片
我們按照上面的方法,可以得出節(jié)點(diǎn)數(shù)量是13,那么最終的圈復(fù)雜度就等于13 + 1 = 14,圈復(fù)雜度是14,值得注意的是,其中的&&也會(huì)被算作節(jié)點(diǎn)之一。
2.2 使用工具
對(duì)于golang我們可以使用gocognit來(lái)判定圈復(fù)雜度,你可以使用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,來(lái)計(jì)算該代碼的圈復(fù)雜度。
$ gocognit test.go
6 main main test.go:11:1
表示main包的main方法從11行開(kāi)始,其計(jì)算出的圈復(fù)雜度是6。
3. 如何降低圈復(fù)雜度
這里其實(shí)有很多很多方法,然后各類(lèi)方法也有很多專(zhuān)業(yè)的名字,但是對(duì)于初了解圈復(fù)雜度的人來(lái)說(shuō)可能不是那么好理解。所以我把如何降低圈復(fù)雜度的方法總結(jié)成了一句話那就是——“盡量減少節(jié)點(diǎn)判定法中節(jié)點(diǎn)的數(shù)量”。
換成大白話來(lái)說(shuō)就是,盡量少寫(xiě)if、else、while、case這些流程控制語(yǔ)句。
其實(shí)你在降低你原本代碼的圈復(fù)雜度的時(shí)候,其實(shí)也算是一種重構(gòu)。對(duì)于大多數(shù)的業(yè)務(wù)代碼來(lái)說(shuō),代碼越少,對(duì)于后續(xù)維護(hù)閱讀代碼的人來(lái)說(shuō)就越容易理解。
簡(jiǎn)單總結(jié)下來(lái)就兩個(gè)方向,一個(gè)是拆分小函數(shù),另一個(gè)是想盡辦法少些流程控制語(yǔ)句。
3.1 拆分小函數(shù)
拆分小函數(shù),圈復(fù)雜度的計(jì)算范圍是在一個(gè)function內(nèi)的,將你的復(fù)雜的業(yè)務(wù)代碼拆分成一個(gè)一個(gè)的職責(zé)單一的小函數(shù),這樣后面閱讀的代碼的人就可以一眼就看懂你大概在干嘛,然后具體到每一個(gè)小函數(shù),由于它職責(zé)單一,而且代碼量少,你也很容易能夠看懂。除了能夠降低圈復(fù)雜度,拆分小函數(shù)也能夠提高代碼的可讀性和可維護(hù)性。
比如代碼中存在很多condition的判斷。
其實(shí)可以優(yōu)化成我們單獨(dú)拆分一個(gè)判斷函數(shù),只做condition判斷這一件事情。
圖片
3.2 少寫(xiě)流程控制語(yǔ)句
這里舉個(gè)特別簡(jiǎn)單的例子。
圖片
其實(shí)可以直接優(yōu)化成下面這個(gè)樣子。
圖片
例子就先舉到這里,其實(shí)你也發(fā)現(xiàn),其實(shí)就像我上面說(shuō)的一樣,其目的就是為了減少if等流程控制語(yǔ)句。其實(shí)換個(gè)思路想,復(fù)雜的邏輯判斷肯定會(huì)增加我們閱讀代碼的理解成本,而且不便于后期的維護(hù)。所以,重構(gòu)的時(shí)候可以想辦法盡量去簡(jiǎn)化你的代碼。
那除了這些還有沒(méi)有什么更加直接一點(diǎn)的方法呢?例如從一開(kāi)始寫(xiě)代碼的時(shí)候就盡量去避免這個(gè)問(wèn)題。
4. 使用go-linq
我們先不用急著去了解go-linq是什么,我們先來(lái)看一個(gè)經(jīng)典的業(yè)務(wù)場(chǎng)景問(wèn)題。
從一個(gè)對(duì)象列表中獲取一個(gè)ID列表
如果在go中,我們可以這么做。
圖片
略顯繁瑣,熟悉Java的同學(xué)可能會(huì)說(shuō),這么簡(jiǎn)單的功能為什么會(huì)寫(xiě)的這么復(fù)雜,于是三下五除二寫(xiě)下了如下的代碼。
圖片
上圖中使用了Java8的新特性Stream,而Go語(yǔ)言目前還無(wú)法達(dá)到這樣的效果。于是就該輪到go-linq出場(chǎng)了,使用go-linq之后的代碼就變成了如下的模樣。
圖片
怎么樣,是不是看到Java 8 Stream的影子,重構(gòu)之后的代碼我們暫且不去比較行數(shù),從語(yǔ)意上看,同樣的清晰直觀,這就是go-linq,我們用了一個(gè)例子來(lái)為大家介紹了它的定義,接下來(lái)簡(jiǎn)單介紹幾種常見(jiàn)的用法,這些都是官網(wǎng)上給的例子。
4.1 ForEach
與Java 8中的foreach是類(lèi)似的,就是對(duì)集合的一個(gè)遍歷。
圖片
首先是一個(gè)From,這代表了輸入,夢(mèng)開(kāi)始的地方,可以和Java 8中的stream劃等號(hào)。
然后可以看到有ForEach和ForEachT,F(xiàn)orEachIndexed和ForEachIndexedT。前者是只遍歷元素,后者則將其下標(biāo)也一起打印了出來(lái)。跟Go中的Range是一樣的,跟Java 8的ForEach也類(lèi)似,但是Java 8的ForEach沒(méi)有下標(biāo),之所以go-ling有,是因?yàn)樗约河涗浟艘粋€(gè)index,F(xiàn)orEachIndexed源碼如下。
圖片
其中兩者的區(qū)別是啥呢?我認(rèn)識(shí)是你對(duì)你要遍歷的元素的類(lèi)型是否敏感,其實(shí)大多數(shù)情況應(yīng)該都是敏感的。如果你使用了帶T的,那么在遍歷的時(shí)候go-ling會(huì)將interface轉(zhuǎn)成你在函數(shù)中所定義的類(lèi)型,例如fruit string。
否則的話,就需要我們自己去手動(dòng)的將interface轉(zhuǎn)換成對(duì)應(yīng)的類(lèi)型,所以后續(xù)的所有的例子我都會(huì)直接使用ForEachT這種類(lèi)型的函數(shù)。
4.2 Where
可以理解為SQL中的where條件,也可以理解為Java 8中的filter,按照某些條件對(duì)集合進(jìn)行過(guò)濾。
圖片
上面的Where篩選出了字符串長(zhǎng)度大于6的元素,可以看到其中有個(gè)ToSlice,就是將篩選后的結(jié)果輸出到指定的slice中。
4.3 Distinct
與你所了解到的MySQL中的Distinct,又或者是Java 8中的Distinct是一樣的作用,去重。
4.3.1 簡(jiǎn)單場(chǎng)景
4.3.2 復(fù)雜場(chǎng)景
當(dāng)然,實(shí)際的開(kāi)發(fā)中,這種只有一個(gè)整形數(shù)組的情況是很少的,大部分需要判斷的對(duì)象都是一個(gè)struct數(shù)組。所以我們?cè)賮?lái)看一個(gè)稍微復(fù)雜一點(diǎn)的例子。
圖片
上面的代碼是對(duì)一個(gè)products的slice,根據(jù)product的Code字段來(lái)進(jìn)行去重。
4.4 Except
對(duì)兩個(gè)集合做差集。
4.4.1 簡(jiǎn)單場(chǎng)景圖片
4.4.2 復(fù)雜場(chǎng)景圖片
4.5 Intersect
對(duì)兩個(gè)集合求交集。
4.5.1 簡(jiǎn)單場(chǎng)景圖片
4.5.2 復(fù)雜場(chǎng)景圖片
4.6 Select
從功能上來(lái)看,Select跟ForEach是差不多的,區(qū)別如下。
Select 返回了一個(gè)Query對(duì)象
ForEach 沒(méi)有返回值
在這里你不用去關(guān)心Query對(duì)象到底是什么,就跟Java8中的map、filter等等控制函數(shù)都會(huì)返回Stream一樣,通過(guò)返回Query,來(lái)達(dá)到代碼中流式編程的目的。
4.6.1 簡(jiǎn)單場(chǎng)景
圖片
select簡(jiǎn)單場(chǎng)景
其中SelectT就是遍歷了一個(gè)集合,然后做了一些運(yùn)算,將運(yùn)算之后的結(jié)果輸出到了新的slice中。
SelectMany為集合中的每一個(gè)元素都返回一個(gè)Query,跟Java 8中的flatMap類(lèi)似,flatMap則是為每個(gè)元素創(chuàng)建一個(gè)Stream。簡(jiǎn)單來(lái)說(shuō)就是把一個(gè)二維數(shù)組給它拍平成一維數(shù)組。
4.6.2 復(fù)雜場(chǎng)景圖片
4.7 Group圖片
Group根據(jù)指定的元素對(duì)結(jié)合進(jìn)行分組,Group`的源碼如下。
圖片
Key就是我們分組的時(shí)候用key,Group就是分組之后得到的對(duì)應(yīng)key的元素列表。
好了,由于篇幅的原因,關(guān)于go-linq的使用就先介紹到這里,感興趣的可以去go-linq官網(wǎng)查看全部的用法。
5. 關(guān)于go-linq的使用
首先我認(rèn)為使用go-linq不僅僅是為了“逃脫”檢測(cè)工具對(duì)圈復(fù)雜度的檢查,而是真正的通過(guò)重構(gòu)自己的代碼,讓其變的可讀性更佳。
舉個(gè)例子,在某些復(fù)雜場(chǎng)景下,使用go-linq反而會(huì)讓你的代碼更加的難以理解。代碼是需要給你和后續(xù)維護(hù)的同學(xué)看的,不要盲目的去追求低圈復(fù)雜度的代碼,而瘋狂的使用go-linq。
我個(gè)人其實(shí)只傾向于使用go-linq對(duì)集合的一些操作,其他的復(fù)雜情況,好的代碼,加上適當(dāng)?shù)淖⑨專(zhuān)攀遣唤o其他人(包括你自己)挖坑的行為。而且并不是說(shuō)所有的if else都是爛代碼,如果必要的if else能夠大大增加代碼的可讀性,何樂(lè)而不為?(這里當(dāng)然說(shuō)的不是那種滿屏各種if else前套的代碼)
到此,相信大家對(duì)“怎么降低代碼的圈復(fù)雜度”有了更深的了解,不妨來(lái)實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。