一步步降低代码复杂度
一步步降低代码复杂度
目录
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<ItemSum> 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<ItemSum> 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,然后给抽取的方法起一个新的名字。