解决代码嵌套严重提示的方法:
关于复杂度的计算可以参考 Cognitive Complexity 认知复杂度
简单来讲就是以下几点:
- if/else/for/while 各记一点, if语句相当于一个if
- 嵌套一层加1点
- try/catch/finally 只有catch记一点,其它不记, catch嵌套在其它里面不记嵌套, 其它嵌套在catch里面需要记嵌套.
- 连续的||记一点,连续的&&记一点
- break/continue 记一点,不考虑嵌套
例如:
if(a || b || c) { //+2, if +1, 连续|| +1
}
if (a || b && d || e) { //+4, if +1, 剩余的||和&&不连续, 所以各 +1
}
if(a) { //+1
try {
while (b) { //+2, 其中嵌套1层
if (c || f) { //+4, 其中嵌套2层, || +1, ||不考虑嵌套
int s =1;
break; //+1
}
}
} catch (e) { // +1 不考虑嵌套
if(h) { //+3 嵌套2层(if和catch)
}
}
}
//以上总共复杂度为18
I recently gave an answer to a very similar question going a little more into detail about how cognitive complexity works (see https://stackoverflow.com/a/62867219/7730554).
But in general, I think it is important to understand that cognitive complexity is increased even more if there are nested conditions. This calculation is done this way because the human brain can deal a lot better with statements written in sequence rather than with nested conditions. So for each conditional statement (if, switch, for loop, etc.) there will be a +1 added to the complexity value. But for each nested condition another +1 is added on top of the last level. That means, an if inside an if will not only add +1, but +2. An if, inside an if, inside an if will than result in +1 for the first if, +2 for the second and +3 for the third if condition. If you want to dig deeper into this I recommend taking a look at: https://www.sonarsource.com/docs/CognitiveComplexity.pdf
So let's analyze where the high complexity values from your method originate first:
function isEmpty(val) {
if (val == null) return true // +1
if ('boolean' == typeof val) return false // +1
if ('number' == typeof val) return val === 0 // +1
if ('string' == typeof val) return val.length === 0 // +1
if ('function' == typeof val) return val.length === 0 // +1
if (Array.isArray(val)) return val.length === 0 // +1
if (val instanceof Error) return val.message === '' // +1
if (val.toString == toString) { // +1
switch (val.toString()) { // +2
case '[object File]':
case '[object Map]':
case '[object Set]': {
return val.size === 0
}
case '[object Object]': {
for (var key in val) { // +3
if (has.call(val, key)) return false // +4
}
return true
}
}
}
return false
}
If you look at the comments I added you can easily see where the most problematic code concerning cyclomatic complexity is located. This also relates to the human readabilty of the code.
So one simply step to increase readability and at the same time reduce congnitive complexityis to look for options of "early returns".
To illustrate this, I simply inverted the statement *if (val.toString == toString)" to immediately return false if *val.toString != toString":
function isEmpty(val) {
if (val == null) return true // +1
if ('boolean' == typeof val) return false // +1
if ('number' == typeof val) return val === 0 // +1
if ('string' == typeof val) return val.length === 0 // +1
if ('function' == typeof val) return val.length === 0 // +1
if (Array.isArray(val)) return val.length === 0 // +1
if (val instanceof Error) return val.message === '' // +1
if (val.toString != toString) { // +1
return false;
}
switch (val.toString()) { // +1
case '[object File]':
case '[object Map]':
case '[object Set]': {
return val.size === 0
}
case '[object Object]': {
for (var key in val) { // +2
if (has.call(val, key)) return false // +3
}
return true
}
}
}
Now the last switch statement can be executed outside the if statement and we reduced the nesting level by one. With that simple change the cognitive complexity has now dropped to 14instead of 17.
You could then even go a step further and change the last case statement by extracting the return value into a variable and either extract a separate method out of the code block. This would reduce the complexity of the isEmpty() method even more.
And besides from extracting a method you can also use a declarative approach and use, for instance the Array method find() which would reduce the cognitive complexity even more.
To illustrate the idea I did both:
function isEmpty(val) {
if (val == null) return true // +1
if ('boolean' == typeof val) return false // +1
if ('number' == typeof val) return val === 0 // +1
if ('string' == typeof val) return val.length === 0 // +1
if ('function' == typeof val) return val.length === 0 // +1
if (Array.isArray(val)) return val.length === 0 // +1
if (val instanceof Error) return val.message === '' // +1
if (val.toString != toString) { // +1
return false;
}
return checkForComplexTypes(val)
}
function checkForComplexTypes(val) {
var result = null
switch (val.toString()) { // +1
case '[object File]':
case '[object Map]':
case '[object Set]': {
result = val.size === 0
}
case '[object Object]': {
result = Object.keys(val).find(key => has.call(val, key))
}
return result
}
}
This should bring down the cognitive complexity of the isEmpty() method to 8 and the whole code including the extracted checkForComplexTypes() function to a complexity score of 9.
Note: JavaScript is not my major language at the moment so I cannot fully guarantee the correctness of the last refactoring step.