This context discusses refactoring code to pass SonarQube's cognitive complexity checks, focusing on the benefits of using code quality tools and providing a walkthrough of reducing cognitive complexity in a specific method.
Abstract
The context begins by highlighting the importance of code quality tools like SonarQube in improving codebase quality and security. It then focuses on cognitive complexity, a metric used by SonarQube to analyze code complexity. The author provides a walkthrough of refactoring a method with high cognitive complexity, using principles from Martin Fowler's book "Refactoring: Improving the Design of Existing Code." The refactoring process involves analyzing and simplifying conditional statements, merging duplicated logic, and removing nested iterations to improve code readability and maintainability.
Opinions
Code quality tools like SonarQube are essential for improving codebase quality and security.
Cognitive complexity is an important metric for analyzing code complexity and maintainability.
Refactoring code to reduce cognitive complexity can improve code readability and maintainability.
Simplifying conditional statements and removing duplicated logic are effective strategies for reducing cognitive complexity.
Following principles from Martin Fowler's book "Refactoring: Improving the Design of Existing Code" can help guide the refactoring process.
Refactoring code can lead to a cleaner, more readable, and comprehensive function with fewer lines of code.
Refactoring can also result in a less complex code with little cognitive overhead and a highly maintainable and extendable method.
Refactoring Code to Pass Sonar Qube’s Cognitive Complexity Checks
SonarQube and similar code quality and security tools are so much blessing
SonarQube and other quality check software are a blessing.
Without them, we end up deploying code with an awful lot of bugs, dead code, unreachable code, code smells, security vulnerabilities, and much more.
A lot of developers’ initial impression and experience with code quality tools is that they introduce needless friction and bottlenecks to the development and deployment processes.
We consider the friction and bottlenecks introduced to our development and integration workflow as a slash to developer productivity.
For now, you are pardoned to hold on to this strong premature consideration until you experience the substantial rewards from these tools.
Integrating code quality tools such as SonarQube into your development process can from the onset seem frustrating and counter-productivity, since you are suddenly responsible for fixing all the issues that are being flagged by the tool. Issues that must be resolved in order to pass CI/CD pipeline checks.
A lot of these issues would have made their way into production and later create run-time issues, code comprehension and maintenance overheads. With tools like SonarQube you can address these issues upfront, know about them without even trying.
SonarQube and other tools alike run code quality checks and ensure the quality and security of your codebase is solid.
Focusing on Cognitive Complexity
One of the quality checks SonarQube performs is analyzing your codebase for code with high cognitive overhead.
SonarQube’s cognitive complexity metrics checks and scores the complexity of code constructs. Higher scores suggest you will have a bigger impediment when you attempt to understand the control flow of the application.
Cognitive complexity increases developer effort in understanding code and furthermore maintaining or extending it.
Sonar has an elaborate rule for measuring the complexity of code. Check out this white paper. Any construct that breaks the linear flow of the application adds to the cognitive complexity score.
Common constructs that add to SonarQube’s cognitive complexity are
Nested conditional and iterations
Iterations
Conditionals with more than one different logical operators
All functions and code blocks with cognitive complexities are flagged by SonarQube with a report similar to below
Photo by Author: A screenshot of Sonar raised SonarQube Cognitive Complexity
In this article, we will walk through how to reduce the cognitive complexity of the create(...) method in the above screenshot.
We will together refactor this method to reduce the cognitive complexity from 28 to at least the acceptable minimum, which is 15.
At the end of this refactoring, we will invariably end up with the following benefits:
A clean, readable and comprehensive function
A less complex code with little cognitive overhead
A method with fewer lines of code
A highly maintainable and extendable method
Meet a Method With Cognitive Complexity Issues
The code below has a SonarQube cognitive complexity score of 25, as earlier seen in the report above.
What Is This Method Doing
create(...) method belongs to a Cheque class from a custom Odoo ERP module developed to handle cheque clearing.
The method create(...) overrides and creates a Partner Cheque Account if a bank branch code branch_code is present in the vals argument passed to the method. It checks and creates this Partner Cheque Account when it does not already exist for the account number acc_no and bank branch branch
A cheque is ultimately created with an existing Partner Cheque Account or a newly created Partner Cheque Account.
Let’s Start By Analyzing and Refactoring
We will start this refactoring exercise by first analyzing the function and pointing out the cognitive related issues and then discuss their resolutions.
Conditional Guards
Line #3 to #7 [ fromCode Snippet #1 ] enforces conditional guards, which are ideal for terminating program execution when certain conditions and application states are not met.
Line #3[ fromCode Snippet #1 ] checks for the key amount in the vals dictionary. The primary action of the create(...) method can not be executed in the absence of these two keys, hence the function terminates with an appropriate ‘ValidationError’ exception raised.
Considering that both conditional guards evaluate the presence of the amount and also checks if the amount value is greater than zero, we can leave these two conditional guards as two separate conditionals if the granularity in feedback is important as already being achieved.
If not, we can consider merging the two into one and generalising the exception message.
Merging the two conditional guards into one, Line #3 to #7 [ fromCode Snippet #1 ] results in the more shortened and readable conditional below.
Simplifying and Merging Conditionals
Line #12 to #17[ fromCode Snippet #1 ] are conditionals we can simplify to increase readability.
You notice there is an evaluation for the presence of the key internal_hold and the truthiness of the its value vals['internal_hold'].
If internal_hold is present and is True, the vals dictionary is updated with a new key state with value internally_held
We also notice that the elif and else block assigns the same state draft to vals['state'] regardless of the difference in their conditional logic.
So here, we will merge the two conditionals.
Refactoring Line #12 to #17 [ fromCode Snippet #1 ] with our current rationale simplifies the conditional statements to below
We could have also employed Python’s ternary operator to achieve the above refactoring with a single statement like so
Merging & Removing Duplicated Logic
A keen observation will bring to our attention the duplication of the code for retrieving a cheque in the nested if-else statements in Line #19 to Line #63[ from Code Snippet #1 ]
Although the domain filters for retrieving a cheque on Line #3 and Line #34 of the snippet from Line #19 to Line #63[ from Code Snippet #1 ] are different, the mechanism is the same, and so can be achieved with a single call to the ORM self.search(...) method.
cheques= self.search([<domain-filters>])
The above code exists in both if and else clauses; and this duplication is because each has to call self.search(...) with a different list of domain filters.
We can simplify this code by moving upwards self.search(...) and passing in a domain filter list prepared based on whether key branch_code exists in vals
Dong this we end up this revision:
In this revision, we created the general domain filter cheque_exists_domain_filters and dynamically extended the filters appropriately based on which of the conditionals are satisfied.
This revision now shows a single call to self.search(cheque_exists_domain_filter)
Doing this flattens our control flow and removes the two levels of nested if statements we had from the original create(...)method [fromCode Snippet #1 ]
Now that we’ve moved self.search(cheque_exist_domain_filter upwards, we can also implement conditional guard, as achieved Line #9 in our revised code, raising ValidationError exception when self.search(cheque_exist_domain_filter) returns some cheques, which is our pre-condition to not proceed with creating a Partner Cheque Account.
Final Refactored Method
This final revision of the create(...)method can be refactored further by applying other refactorings such as “Method Extraction”.
But the goal of this article was to consider the minimum refactorings we can make to have this method pass SonarQube’s cognitive complexity checks.
I hope you enjoyed this refactoring exercise.
All the best with your software development journey.
You may also want to read these other articles from me
Hey thank you for taking the time to read this article. Before you go, there’s something I want you to know. You can support my writing by joining Medium as a paying member with my referral link. With only $5you can have full access to content like what you just read and more. If you join today, I’ll make a small commission on your $5 membership fee without any additional cost to you.