
在抱怨别人代码晦涩难懂的时候,殊不知在不久的以后,自己也会成为别人抱怨的对象,从此刻开始改变
公司通过sonarqube来检测代码质量,最近着手修改几个项目的 1k+ 的bug、16K+ 异味问题(奔溃),其中出现最多的问题主要有:
-
代码规范类错误/风格不统一:区分 let 和 const的使用、方括号对齐、最后一个函数不能加逗号(vue、react)等等
- 随着项目体积变大、项目经手人增多,这类问题可以在项目中加入ESlint、Tslint、Prettier这类优秀的代码检测工具来规范整体的代码规范和质量
-
单个代码块复杂度过高,导致理解起来困难,改动风险高
如果判断代码块的复杂度,以及如何降低复杂度,是我们这次讨论的重点!
认知复杂度
规则定义
认知复杂度(Cognitive complexity)是衡量功能的控制流程难易程度的一种度量。 具有高度认知复杂性的功能将难以维护.
这是sonarqube针对代码复杂度的一个检验规则,函数的认知复杂度不应过高,我们知道,不管在原生js、vue、react,函数是我们最常见的代码模块,而sonarqube以一个函数为单元进行计算复杂度,他内置一套计算方式,并设置阈值15作为分数上限,也就是单个代码块的复杂度分数不能超过15.
sonarqube提供了一个21页的pdf进行详细解释计分方式,有需要可以看这里,我下面重点提文档中几个常见的场景
计算方法
常见的加分情况主要有三种:
-
增量
- if, else if, else, 三元运算符
- switch
- for, foreach
- while, do while
- catch
- break LABEL, continue LABEL
- 二进制逻辑运算符序列(&&和||):这个比较特殊,后面单独讲
- 递归循环中的每种方法
-
嵌套级别(以下结构可增加嵌套级别)
- if, else if, else, 三元运算符
- switch
- for, foreach
- while, do while
- catch
- 嵌套方法和类似方法的结构(例如lambda)
// (部分例子)
demo1 (obj) {
if (obj) { // +1
return true
}
for (let i = 1; i < 10; i++) { // +1
if (obj.name) { // +2(if + 1; nesting +1)
if (obj.name.class) { // +3(if +1; nesting +2)
return true
}
}
}
tableData.map(item => {
item.name = this.getuserName(item.id)
item.type = item.type ? Moment(new Date(item.type)).format('YYYY-MM-DD') : '' // +2 (三元符 +1; nesting +1)
})
}
以上列举了部分增量的加分例子,在未发生嵌套的情况下,单一的增量语句是 +1 分,但是如果增量语句发生了嵌套关系,分值就要进行累加,也就是说,如果当前增量语句被其他增量语句嵌套了,嵌套n层则多 +n,这部分的分值就是嵌套(nesting)所带来的额外分值,可想而知,如果嵌套层数过多,那么函数复杂度将会很高,
第一段代码:单一的if语句,是单一增量,则+1
第二段代码:存在嵌套结构,for 本身增量 +1;第一个 if 语句被 fir嵌套一层,则+2分,其中一分是嵌套带来的额外分值;同理,第二个 if 语句则 +3
第三段代码:ableData.map是嵌套方法和类似方法的结构,不属于增量,不加分,但后面的三元符是属于增量和嵌套级别,因此 +2 ,其中一分是嵌套带来的额外分值
// 二进制逻辑运算符序列(&&和||)
demo2 (obj) {
if (obj && obj.name === 'thomas') { // +2 ()
return true
}
if (obj) { // +1
let flag3 = obj && obj.name === 'lucy' // +1
}
let flag = obj && obj.name === 'nacy' // +1
let flag2 = obj && obj.name || obj.age // +2
let flag3 = obj && obj.name || obj.age && obj.address // +3
let flag4 = obj && obj.name && obj.age && obj.address // +1
}
二进制逻辑运算符序列包括 && 和 || ,由于它属于增量, 则 +1 ,但它不属于嵌套级别,因此即使它在嵌套层内,也不会有额外的嵌套分值。
但是它还有一个特殊的地方是相邻逻辑运算符不同会造成额外加分,看例子说明:
第一段代码:单一的if语句,+1;单一的 &&,+1
第二段代码:虽然被 if 语句嵌套,但是依旧仅 +1
第三段代码:第一行只有一个&&,+1;第二行的&& 相邻是 ||,因为相邻的逻辑运算符不同,导致&& +1,|| +1,最终得分为2;第三行同理,&& +1; || +1, && +1;第四行,所有的逻辑运算符的相邻都没有不同的逻辑运算符,因此整体+1
为了加深理解,下面给出一个复杂的例子
function fetch (id) {
$page.getSomething({ id }).then(res => {
if (res && res.data && res.data.userList || res.data.infoList) { // +4 (if +1; && +1; || +1; nesting +1)
if (this.$route.query.flag === 'one') { // +3(if +1; nesting +2)
for (let [k, v] of Object.entries(userList)) { // +4 (if +1; nesting +3)
this.$set(v, '$flag', k)
for (let [m, n] of Object.entries(this.ee)) { // +5 (if +1; nesting +4)
this.$set(n, '$flag', m)
if (v.emplid === n.emplid) { // +6 (if +1; nesting +5)
emplList.push(v)
}
}
}
} else if (this.$route.query.flag === 'two'){ // (if +1; nesting +1)
console.log('two')
}
}
this.retrieve(res.data)
const formNo = res.data.bpmsFormNo
this.retrieveHistory(formNo)
this.isLoading = false
}).catch(() => { // +1
this.isLoading = false
})
}
如果不明白加分细则,可以在评论区提问,我看到会解答。
上面的示例仅作为理解加分算法的demo,实际的项目代码比这个复杂的多,恶心起来也会加倍。
降低认知复杂度
示例代码仅仅代表一种思想, 改写也不是必要的,还是要根据实际情况判断是否改写更合适
-
嵌套结构的语句会受到嵌套的额外加分,若当前语句较简单,且处于嵌套中,可以使用下面方式进行改写

-
一行代码中逻辑运算符尽量唯一,不要同时出现不同的逻辑运算符。

-
将复杂冗余的条件判断用switch语句来替换

- 函数要遵循单一性的原则,不要一个函数的实现功能太复杂,只做一件事。具体如何去抽象出多个函数,还需要根据业务场景进行重构

- 如果判断逻辑比较复杂,以函数取代判断条件,并以函数取代执行函数,最后以三元运算符将几个函数串起来,一方面增强了代码的语义化,另一方面提高了代码可读性,建议将比较复杂的执行逻辑封装为一个函数

- 将结束函数的判断条件语句前置,可以方便理解,也可以减少不必要的代码执行
屏幕快照 2019-11-23 下午3.38.34

总结
希望看完本篇文章能对你有所帮助,
文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。