Keywords: Cognitive Complexity | Code Refactoring | SonarQube
Abstract: This article explores the differences between cognitive complexity and cyclomatic complexity, analyzes the causes of high-complexity code, and demonstrates through practical examples how to reduce cognitive complexity from 21 to 11 using refactoring techniques such as extract method, duplication elimination, and guard clauses. It explains SonarQube's scoring mechanism in detail, provides step-by-step refactoring guidance, and emphasizes the importance of code readability and maintainability.
Difference Between Cognitive Complexity and Cyclomatic Complexity
In software quality analysis, cognitive complexity and cyclomatic complexity are two key but distinct metrics. Cyclomatic complexity primarily measures the number of independent paths in code, typically calculated through conditional statements and loops. Cognitive complexity, however, focuses more on the burden code places on human understanding, considering not only conditional statements but also the additional cognitive load from nested structures.
According to SonarQube documentation, cognitive complexity calculation rules include: +1 base score for each conditional statement, +1 extra for each level of nesting, and logical operators (e.g., &&, ||) also increase complexity. For example, the following code has a cognitive complexity of 6 but a cyclomatic complexity of only 4:
if (someVariableX > 3) { // +1
if (someVariableY < 3) { // +2 (nesting +1)
if (someVariableZ === 8) { // +3 (nesting +2)
someResult = someVariableX + someVariableY - someVariableZ;
}
}
}This difference stems from cognitive complexity simulating the difficulty the human brain experiences when processing nested logic. Deep nesting makes code hard to follow and understand, even if the number of paths is low.
Case Analysis of High-Complexity Code
The original code had a cognitive complexity of 21, mainly due to multiple logical operators and conditional checks. Here is a detailed breakdown of its complexity:
this.deviceDetails = this.data && { ...this.data.deviceInfo } || {}; // +2
if (this.data && this.data.deviceInfo) { // +2 (if statement +1, logical operator +1)
// Multiple property assignments, each with logical operators
managerId: device.deviceManager && device.deviceManager.managerId || null, // +2
locationId: device.location && device.location.locationId || null, // +2
driver_id: driver && driver.driverId || null, // +2
// Other simple assignments
} else { // +1
// Other statements
}The code contained numerous repeated logical patterns, such as checking if object properties exist and setting default values. This duplication not only increased complexity but also reduced maintainability.
Refactoring Strategies and Implementation Steps
The core strategy for reducing cognitive complexity is extract method. By encapsulating repeated logic into independent functions, the number of conditional statements in the main method can be reduced.
First, create helper functions for common logic:
function getInfoItem(infoItem, defaultValue = '') {
return infoItem || defaultValue; // complexity +1
}
function getManagerId(device) {
return device.deviceManager && device.deviceManager.managerId || null; // complexity +2
}
function deviceInfoAvailable() {
return this.data && this.data.deviceInfo; // complexity +1
}Second, refactor the main method to use these functions:
this.deviceDetails = getDeviceInfo();
if (deviceInfoAvailable()) {
this.getSessionInfo();
const { device, driver, ipAddress, port, active, connectionType } = this.data.deviceInfo;
this.deviceDetails = {
name: getInfoItem(device.name),
manufacturer: getInfoItem(device.manufacturer),
managerId: getManagerId(device),
// Other properties handled similarly
};
this.oldDeviceDetails = { ...this.deviceDetails };
this.deviceLocation = getDeviceLocation(device);
} else {
// Handle else branch
}This refactoring reduces cognitive complexity from 21 to 12 while eliminating code duplication.
Advanced Refactoring: Guard Clauses Technique
Further complexity reduction can be achieved using guard clauses, also known as "early return" pattern. By inverting conditional logic, the else branch can be eliminated, reducing complexity by one point.
this.deviceDetails = getDeviceInfo();
if (!deviceInfoAvailable()) {
// Handle original else branch logic
return;
}
// Main logic continues without else branch
this.getSessionInfo();
const { device, driver, ipAddress, port, active, connectionType } = this.data.deviceInfo;
this.deviceDetails = {
// Property assignments
};
this.oldDeviceDetails = { ...this.deviceDetails };
this.deviceLocation = getDeviceLocation(device);This optimization further reduces cognitive complexity to 11. Guard clauses make the code flatter and easier to understand in terms of execution flow.
Refactoring Effects and Best Practices
After the above refactoring, the code not only meets SonarQube's complexity requirements (reduced from 21 to below 15) but also achieves the following improvements:
- Improved Readability: Extracted methods have clear names like
getManagerId, making the code self-documenting. - Enhanced Maintainability: Logic is centralized, requiring changes only in a single function.
- Eliminated Code Duplication: Property checking logic previously scattered throughout is now unified.
In practical development, it is recommended to:
- Regularly use tools like SonarQube to check code complexity.
- Prioritize extracting repeated logic, especially code blocks containing conditional checks.
- Consider using guard clauses to simplify conditional branches.
- For complex data objects, create specialized classes or services to encapsulate related logic.
Through systematic refactoring, developers can not only address static analysis tool warnings but also fundamentally improve code quality, making it easier to understand, test, and maintain.