Table of Contents
1 Sonar认知复杂度计算规则
2 降低复杂度
2.1 降低复杂度前的准备
2.2 一个for循环只做一件事
2.3 减少for循环中的if else、continue
2.4 抽离try/catch
2.5 for循环内容不宜过长
2.6 验证重构
3. 通过idea一秒钟实现重构
1 Sonar认知复杂度计算规则
这里的认知复杂度特指代码分析工具Sonar的认知复杂度。
Sonar要求认知复杂度低于15。首先了解一下认知复杂度的计算规则。
Cognitve Complexity的计算: (1)&&、|| 条件判断符号 +1 (2)if、else if、else、switch 分支语句+1 (3)for、while、do while 循环语句+1 (4)catch 捕获异常语句+1 (5)break、continue 中断语句+1 (6)如果if、for、while、do while、catch存在嵌套时,里层的语句相对于外层+1 |
来看两个官方示例
2 降低复杂度
了解了复杂度的规则,我们的主要工作就是围绕着如何优雅的减少条件判断来进行。
这里以某方法 restrictionNumLimit 为例展示降低复杂度的若干步骤。
Sonar扫描显示该方法的复杂度为20
restrictionNumLimit逻辑为判断当前下单的商品数量是否超过限购。
2.1 降低复杂度前的准备
首先为restrictionNumLimit编写单元测试,覆盖各个条件边界。
为此编写了一个代码行数5倍于restrictionNumLimit的单元测试,重构前测试全绿。
没有单元测试的重构如同盲人开车,不翻车看运气。
2.2 一个for循环只做一件事
查看代码,for循环里面有if else。逻辑为根据商品Id累加商品数量。
if else,加上for循环,总共贡献了3个复杂度。
//累加商品id相同的sku数量 for (OrderSkuQuery orderSkuQuery : orderSkuQueryList) { if (res.containsKey(orderSkuQuery.getItemsId())) { res.put(orderSkuQuery.getItemsId(), orderSkuQuery.getSkuNum() + res.get(orderSkuQuery.getItemsId())); } else { res.put(orderSkuQuery.getItemsId(), orderSkuQuery.getSkuNum()); } } |
借助 java.util.Collection.stream 、java.util.Map.compute ,在一个表达式里面实现了商品数量的累加,抹除了for循环与if else的认知复杂度。
Map< Long , Integer> res = Maps.newHashMap(); orderSkuQueryList.stream().forEach(orderSkuQuery -> res.compute(orderSkuQuery.getItemsId(), (k, v) -> v == null ? orderSkuQuery.getSkuNum() : v + orderSkuQuery.getSkuNum())); |
2.3 减少for循环中的if else、continue
for循环中的continue,if括号内过长的条件判断都增加了复杂度。
由于continue跟业务处理无关,先在外围通过java.util.stream.Stream.filter把continue从for循环抽离出去,把过长的条件判断抽取到一个方法内。
如下,for循环中已经没有continue跟过长的条件判断。
2.4 抽离try/catch
try/catch代码块丑陋不堪。它们搞乱了代码结构,把错误处理与正常流程混为一谈。最好把try和catch代码块的主体部分抽离出来,另外形成函数。
——《代码整洁之道》
通过把for循环里面的try方法抽取到单独的方法,继续降低复杂度
// 查询历史订单数量,强制查询主库 List itemSumList = null ; try (HintManager hintManager = HintManager.getInstance()) { hintManager.setMasterRouteOnly(); itemSumList = SpringContextHolder.getBean(OrderSkuDao. class ).sumByItemsIdAndUserId(itemsId, openId, 0 , openSource.getCode()); } int hisCount = CollectionUtils.isEmpty(itemSumList) ? 0 : itemSumList.get( 0 ).getSum(); // 重构为 private int getHistoryItemSums(Long itemsId) { List itemSumList = null ; try (HintManager hintManager = HintManager.getInstance()) { hintManager.setMasterRouteOnly(); itemSumList = SpringContextHolder .getBean(OrderSkuDao. class ) .sumByItemsIdAndUserId(itemsId, openId, 0 , openSource.getCode()); } return CollectionUtils.isEmpty(itemSumList) ? 0 : itemSumList.get( 0 ).getSum(); } |
2.5 for循环内容不宜过长
如果一个for循环内容过长只会越来越长。
如下当满足条件:该商品历史购买数量已经超过限购直接就跳出for循环return了,那么return后面的else可以省略。
同时else里面的内容可以抽取到一个方法里面,避免for循环内容过长。
2.6 验证重构
所谓重构(refactoring)是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。
——《重构:改善既有代码的设计》
重构后要保证代码逻辑的正确,如下重构后测试继续全绿。
3. 通过idea一秒钟实现重构
如果一个工具能够为你安全地提取方法,你就不需要自己编写测试去验证提取是否正确。
——《修改代码的艺术》
有时候我们来不及为一个方法写一个超长的单元测试,借助idea的Extract Method功能瞬间完成重构,降低认知复杂度。
如下选中超长的条件判断,右键Refactor,选中Extract Method,然后给抽取的方法起一个新的名字。