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