2020-12-30

博客园Logo
首页
新闻
博问
专区
闪存
班级

代码改变世界
搜索
注册
登录
返回主页
SH的全栈笔记
微信搜一搜「SH的全栈笔记」
博客园
首页
新随笔
联系
订阅
管理
降低代码的圈复杂度——复杂代码的解决之道

本文代码示例以Go语言为例
欢迎微信关注「SH的全栈笔记」
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中,我们可以这么做。
go实现
略显繁琐,熟悉Java的同学可能会说,这么简单的功能为什么会写的这么复杂,于是三下五除二写下了如下的代码。
使用linq重构前
上图中使用了Java8的新特性Stream,而Go语言目前还无法达到这样的效果。于是就该轮到go-linq出场了,使用go-linq之后的代码就变成了如下的模样。
使用go-linq重构后
怎么样,是不是看到Java 8 Stream的影子,重构之后的代码我们暂且不去比较行数,从语意上看,同样的清晰直观,这就是go-linq,我们用了一个例子来为大家介绍了它的定义,接下来简单介绍几种常见的用法,这些都是官网上给的例子。
4.1 ForEach
与Java 8中的foreach是类似的,就是对集合的一个遍历。
image-20201229093033157
首先是一个From,这代表了输入,梦开始的地方,可以和Java 8中的stream划等号。
然后可以看到有ForEach和ForEachT,ForEachIndexed和ForEachIndexedT。前者是只遍历元素,后者则将其下标也一起打印了出来。跟Go中的Range是一样的,跟Java 8的ForEach也类似,但是Java 8的ForEach没有下标,之所以go-ling有,是因为它自己记录了一个index,ForEachIndexed源码如下。
ForEachIndexed源码
其中两者的区别是啥呢?我认识是你对你要遍历的元素的类型是否敏感,其实大多数情况应该都是敏感的。如果你使用了带T的,那么在遍历的时候go-ling会将interface转成你在函数中所定义的类型,例如fruit string。
否则的话,就需要我们自己去手动的将interface转换成对应的类型,所以后续的所有的例子我都会直接使用ForEachT这种类型的函数。
4.2 Where
可以理解为SQL中的where条件,也可以理解为Java 8中的filter,按照某些条件对集合进行过滤。
where用法
上面的Where筛选出了字符串长度大于6的元素,可以看到其中有个ToSlice,就是将筛选后的结果输出到指定的slice中。
4.3 Distinct
与你所了解到的MySQL中的Distinct,又或者是Java 8中的Distinct是一样的作用,去重。
4.3.1 简单场景distinct去重
4.3.2 复杂场景
当然,实际的开发中,这种只有一个整形数组的情况是很少的,大部分需要判断的对象都是一个struct数组。所以我们再来看一个稍微复杂一点的例子。
复杂对象的distinct
上面的代码是对一个products的slice,根据product的Code字段来进行去重。
4.4 Except
对两个集合做差集。
4.4.1 简单场景except简单场景
4.4.2 复杂场景except-复杂场景
4.5 Intersect
对两个集合求交集。
4.5.1 简单场景intersect简单场景
4.5.2 复杂场景intersect复杂场景
4.6 Select
从功能上来看,Select跟ForEach是差不多的,区别如下。
Select 返回了一个Query对象
ForEach 没有返回值
在这里你不用去关心Query对象到底是什么,就跟Java8中的map、filter等等控制函数都会返回Stream一样,通过返回Query,来达到代码中流式编程的目的。
4.6.1 简单场景
select简单场景
select简单场景
其中SelectT就是遍历了一个集合,然后做了一些运算,将运算之后的结果输出到了新的slice中。
SelectMany为集合中的每一个元素都返回一个Query,跟Java 8中的flatMap类似,flatMap则是为每个元素创建一个Stream。简单来说就是把一个二维数组给它拍平成一维数组。
4.6.2 复杂场景selectManyByT-复杂场景
4.7 Groupimage-20201229122918527
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前套的代码)
好了以上就是本篇博客的全部内容了,如果你觉得这篇文章对你有帮助,还麻烦点个赞,关个注,分个享,留个言。
欢迎微信搜索关注【SH的全栈笔记】,查看更多相关文章
标签: 架构
好文要顶 关注我 收藏该文
detectiveHLH
关注 - 0
粉丝 - 40
+加关注
0 0
« 上一篇: 深度图解Redis Cluster原理
posted @ 2020-12-30 09:30 detectiveHLH 阅读(155) 评论(5) 编辑 收藏

评论列表
#1楼 2020-12-30 09:59 达叔
有点太理想化了,想不明白,一个100行的函数,拆成多个函数后,行数只会更多,分支也不见得会减少,咋么复杂度就降低了呢?有点扯淡。还是从设计和算法上减少代码行数是正途。减圈是结果,不是目的,文章好像有点本末倒置了。
支持(0) 反对(0)
#2楼 [楼主] 2020-12-30 10:00 detectiveHLH
@达叔
我个人觉得这只是其中的一种方式,你说的设计和算法是另一种方式,并不冲突
支持(0) 反对(0)
#3楼 2020-12-30 10:29 达叔
@detectiveHLH
我是有点偏激了哈。主要是担心上有政策下有对策,干活的人为了减圈了减圈。
支持(0) 反对(0)
#4楼 [楼主] 2020-12-30 10:31 detectiveHLH
@达叔
这个…如果你把减少圈复杂度作为一个硬性要求,难免上有政策下有对策。如果你是对自己的代码质量很有要求,我觉得这个重构过程会让你有很多收益的。
支持(0) 反对(0)
#5楼 2020-12-30 12:29 coredx
go-linq,既然连go都知道是抄的linq,都不提一嘴linq真正的祖宗其实是C#吗?stream都是抄的linq,还是个半残废。知道为什么有的处理看起来更费劲吗?因为除了C#,其他语言都没有SQL风格语法和匿名类型。大多数编程语言都是命令式语言,和数据处理领域需要的声明式领域特定语言天生八字不合,这都不别扭费劲才是见鬼了,不然专门发明SQL干嘛?闲的?
还有,如果if-else是用来对数据模式进行分类然后选择处理方式,拆成一堆稀碎的小函数除了降低圈复杂度不会对可读性和可维护性有任何好处,这只是自欺欺人。真正的治根之法是引入模式匹配,通过模式匹配语法用最精炼直观的代码描述分类条件,把乱七八糟的嵌套if转化成单个线性表达式。
有些问题不是重构能解决的,必须语言本身进化。重构只能解决低品位码农的低质量代码,但是再牛逼的开发者也不可能解决语言缺陷导致的问题,只能通过语言的进化来解决。这跟牛顿力学永远不可能解决卫星定位误差的问题一样,只有进化到相对论才能触及问题的核心,才能找到解决的办法。
支持(0) 反对(0)
刷新评论刷新页面返回顶部
登录后才能发表评论,立即 登录 或 注册, 访问 网站首页
写给园友们的一封求助信
【推荐】News: 大型组态、工控、仿真、CADGIS 50万行VC++源码免费下载
【推荐】有你助力,更好为你——博客园用户消费观调查,附带小惊喜!
【推荐】博客园x丝芙兰-圣诞特别活动:圣诞选礼,美力送递
【推荐】了不起的开发者,挡不住的华为,园子里的品牌专区
【福利】AWS携手博客园为开发者送免费套餐+50元京东E卡
【推荐】未知数的距离,毫秒间的传递,声网与你实时互动
【推荐】新一代 NoSQL 数据库,Aerospike专区新鲜入驻

相关博文:
· 光圈与景深
· BZOJ2132圈地计划
· 洛谷P2423[HEOI2012]朋友圈
· CF1189BNumberCircle(数字圈)
· [算进]赶牛入圈题解
» 更多推荐…

最新 IT 新闻:
· 知乎到底比B站差哪了?
· 淘宝揭晓年度“丑东西”:羊毛毡买家秀“拔得头丑”
· 太空垃圾增多威胁地球安全?日本研发木制卫星
· 沈向洋:人工智能社区将创造更多工具释放人类创造力
· 业内首创!小米MIUI 12.5无障碍触感新功能发布
» 更多新闻…
公告
昵称: detectiveHLH
园龄: 3年7个月
粉丝: 40
关注: 0
+加关注
< 2020年12月 >
日 一 二 三 四 五 六
29 30 1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31 1 2
3 4 5 6 7 8 9
搜索

找找看

谷歌搜索
我的标签
Java(10)
Redis(8)
后端(5)
Go(4)
Spring-Boot(4)
javascript(4)
WebAssembly(3)
Node(2)
数据结构(2)
算法(2)
更多
随笔档案
2020年12月(4)
2020年11月(1)
2020年10月(1)
2020年9月(1)
2020年7月(2)
2020年6月(1)
2020年4月(1)
2020年3月(1)
2019年12月(2)
2019年10月(3)
2019年7月(2)
2019年6月(5)
2019年5月(3)
2019年4月(1)
2019年3月(2)
更多
最新评论

  1. Re:降低代码的圈复杂度——复杂代码的解决之道
    go-linq,既然连go都知道是抄的linq,都不提一嘴linq真正的祖宗其实是C#吗?stream都是抄的linq,还是个半残废。知道为什么有的处理看起来更费劲吗?因为除了C#,其他语言都没有SQ…
    –coredx
  2. Re:降低代码的圈复杂度——复杂代码的解决之道
    @达叔 这个…如果你把减少圈复杂度作为一个硬性要求,难免上有政策下有对策。如果你是对自己的代码质量很有要求,我觉得这个重构过程会让你有很多收益的。…
    –detectiveHLH
  3. Re:降低代码的圈复杂度——复杂代码的解决之道
    @detectiveHLH 我是有点偏激了哈。主要是担心上有政策下有对策,干活的人为了减圈了减圈。…
    –达叔
  4. Re:降低代码的圈复杂度——复杂代码的解决之道
    @达叔 我个人觉得这只是其中的一种方式,你说的设计和算法是另一种方式,并不冲突…
    –detectiveHLH
  5. Re:降低代码的圈复杂度——复杂代码的解决之道
    有点太理想化了,想不明白,一个100行的函数,拆成多个函数后,行数只会更多,分支也不见得会减少,咋么复杂度就降低了呢?有点扯淡。还是从设计和算法上减少代码行数是正途。减圈是结果,不是目的,文章好像有点…
    –达叔
    阅读排行榜
  6. WebAssembly完全入门——了解wasm的前世今身(44664)
  7. html2canvas关于图片不能正常截取(7518)
  8. 如何正确的在项目中接入微信JS-SDK(6340)
  9. 手把手教你从零开始搭建SpringBoot后端项目框架(2919)
  10. 将你的前端应用打包成docker镜像并部署到服务器?仅需一个脚本搞定(2740)
    评论排行榜
  11. 什么?你竟然还没有用这几个chrome插件?(13)
  12. WebAssembly完全入门——了解wasm的前世今身(12)
  13. 【硬核教程】只需1秒—你也可以有自己的API文档(9)
  14. 简单了解一下K8S,并搭建自己的集群(9)
  15. 小强开饭店-从单体应用到微服务(9)
    推荐排行榜
  16. 简单了解一下K8S,并搭建自己的集群(13)
  17. WebAssembly完全入门——了解wasm的前世今身(13)
  18. 深度图解Redis Cluster原理(8)
  19. 小强开饭店-从单体应用到微服务(6)
  20. 将你的前端应用打包成docker镜像并部署到服务器?仅需一个脚本搞定(6)
    Copyright © 2020 detectiveHLH
    Powered by .NET 5.0 on Kubernetes
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值