Reducing cyclomatic complexity with functional operators
Reducing the cyclomatic complexity in a JavaScript application is a good, quantifiable way to make code more readable and maintainable.
Cyclomatic complexity increases as more control statements are used. For a summary of the factors that influence complexity,
see “Complexity for JavaScript”. Because
it is quantifiable, it can be automatically validated in build setups with eslint.
Bear in mind that a single switch
statement with a large amount of options will have a high cyclomatic complexity but
probably is still perfectly readable. As always, rely on common sense when refactoring.
Control statements that concern one parameter often use switch
, for example:
const foo = 'banana';
switch (foo) {
case 'apple':
console.log('First');
break;
case 'banana':
console.log('Second');
break;
case 'coconut':
console.log('Third');
break;
default:
console.log('Last');
}
// Logs "Second"
A very common way to reduce the cyclomatic complexity with switch
is to use a JavaScript object as a map. Instead of
traversing the switch statement, do a lookup in the map where the key is the condition and the value is the action. For the
example above this could look like this:
const actions = {
apple: _ => console.log('First'),
banana: _ => console.log('Second'),
coconut: _ => console.log('Third')
}
const foo = 'banana';
if(actions.hasOwnProperty(foo)) {
actions[foo]();
} else {
console.log('Last');
}
// Logs "Second"
This does not work directly with more complex conditions. e.g. a composite condition that checks against multiple parameters within one statement or between statements.
function example(error, stdout, stderr, mapping) {
if(stderr && stderr.length > 0 && stderr.indexOf('A problem occurred') > -1) {
console.log('Not found');
} else if(error || (stderr && stderr.length > 0)) {
console.log(`Execution error ${[error, stdout, stderr].join('|')}`);
} else if(!Array.isArray(mapping)) {
console.log(`Invalid configuration ${[error, stdout, stderr, mapping].join('|')}`);
} else if(!stdout || stdout.length <= 0) {
console.log('No output');
} else {
console.log('Result from some process with mapping and stdout');
}
}
example(null,null,'A problem occurred'); // Logs "Not found"
example(new Error()); // Logs "Execution error Error||"
example(null,null,null,null); // Logs "Invalid configuration |||"
example(null,'',null,[]); // Logs "No output"
example(null,'Foo',null,[]); // Logs "Result from some process with mapping and stdout"
In this case a JavaScript Array of objects can be used here, with one property holding the condition and another holding the desired action.
// First part
const actions = [{
condition: _ => !stdout || stdout.length <= 0,
action: _ => console.log('No result')
}, {
condition: _ => !Array.isArray(mapping),
action: _ => console.log('Config invalid')
}, {
condition: _ => error || (stderr && stderr.length > 0),
action: _ => console.log('Error executing')
}, {
condition: _ => stderr && stderr.length > 0 && stderr.indexOf('A problem occurred') > -1,
action: _ => console.log('Not found')
}]
It would be possible to loop over each entry with a for
loop or an iterator (see an example), but this is where the expressiveness of
the functional operators Array.prototype.filter
and Array.prototype.reduce
shines.
// Second part
function example(error, stdout, stderr, mapping) {
const actions = ... // See above
const selectedAction = actions
.filter(action => action.condition())
.reduce((accumulated, currentAction) => {
// By design, reduce will only keep the last match when the accumulator
// is ignored, so the order inside the actions array has significance.
return currentAction.action;
}, function() {
// Else clause
console.log('Result from some process with mapping and stdout');
});
selectedAction();
}
example(null, null, 'A problem occurred'); // Logs "Not found"
example(new Error()); // Logs "Error executing"
example(null, null, null, null); // Logs "Config invalid"
example(null, '', null, []); // Logs "No result"
example(null, 'Foo', null, []); // Logs "Result from some process with mapping and stdout"
A working example can be found in this fiddle.
This shows how to use functional operators to reduce cyclomatic complexity for an if else
condition. Do keep in mind
it may not be beneficial for performance. For instance, do not declare the action array inside of the function as in
the example, as it would redeclare the array each time the function is executed. Another improvement would be to stop
iteration in filter/reduce
as soon as a matching action was found.
Also note that the example with the if else
condition itself is quite readable, so it will not be worth the trade off of
refactoring to resolution through an array of objects in this case.