Cognitive Complexity Vs Cyclomatic Complexity - An Example With C#

Background

 
While writing professional code, code metrics are an important tool to determine that you are writing quality code that would make it easy to test, understand and maintain in the long run, as the code passes from one developer to another. Since every developer has a different style of writing the same logic, it becomes imperative to set up some standard/guidelines or quantitative measures to tell or determine what makes a good quality code. Two of the most relevant ones, that I find very interesting to explore is,
 

Cyclomatic Complexity and it's younger sibling Cognitive Complexity!

 
The concept of Cognitive Complexity was brought in by SonarQube. They wanted to introduce a more contextualized form of measuring the code complexity. While you can read all the artifacts available to you in public domain on both this topic, I would rather summarize it as below, to the best of my understanding of the concepts till now,
 
Cyclomatic Complexity
 
Measures, how difficult it is test the code (i.e. Testability). Alternatively, this measure is a hint of how many distinct test cases you should write to have 100% code coverage. 
 
Cognitive Complexity
 
Measures, how difficult it is to understand or read the code (i.e. human readability).
 
Let me try to explain this using a small example. Consider the following Lines of Code,
  1. switch (input)  
  2. {  
  3.   case 1:  
  4.    //do something  
  5.    break;  
  6.   case 2:  
  7.    //do something else  
  8.    break;  
  9. }  
As per the Cyclomatic complexity, it would measure this as 2. That's because, you would probably write two test cases, one to test each of the cases individually. While this is too simplistic an example to put the point, extrapolate this in your mind, with all the conditional flows, loops etc that you may have written as part of the same function/method. The score would increase linearly. Thus, as per the general score guidelines,
 
 Score  Cyclomatic  Risk Type
 1 to 10  Simple  Not much risk
 11 to 20  Complex  Low risk
 21 to 50  Too complex  Medium risk, attention
 More than 50  Too complex  Can't test, high risk
 
But as per Cognitive Complexity, it would measure this as 1 for all of the switch case options. Remember, it's algorithm measures the readability. The purpose of the switch case is to present multiple lanes of processing and therefore having multiple cases is not complex to comprehend. It is expected to be the nature of the switch capability. An acceptable value is 15 or less for a given method as set by many professional software houses.
 
Now let's start our play to understand the difference of the two. I will work with a simple program of finding whether the input given, is a leap year or not. So, the given year is a leap year, if it's ordinal is exactly divisible by 4. However, if it is also divisible by 100, but not by 400, then it's no longer a leap year. Finally, if it is divisible by 400 as well, then it's again a leap year. Therefore, the test cases are,
 
Leap Year : 1980, 1996, 2000, 2400 etc.
Not a leap year: 1981, 1995, 1700, 1900 etc. 
 
My first raw algorithm is as follows: (I have restricted to taking the input between 1000 and 9999) 
  1. public bool IsLeapYear(int y)  
  2. {  
  3.     bool response = false;  
  4.     if ((y >= 1000) && (y <= 9999))  
  5.     {  
  6.         //if it is divisible by 400 e.g. 1600, 2000, then all other checks are useless. we can return from here  
  7.         if (y % 400 == 0)  
  8.             response = true;  
  9.         //for years like 1700, 1900, 2100 etc. which are not leap years  
  10.         else if ((y % 100 == 0) && (y % 400 != 0))  
  11.             response = false;  
  12.         //finally, if the above two fails, just check if it divisible by 4 for years like 1980,1996 etc  
  13.         else if (y % 4 == 0)  
  14.             response = true;  
  15.     }  
  16.     else  
  17.     {  
  18.         response = false;  
  19.         throw new ArgumentOutOfRangeException("year"" The argument value should be inside 1000 and 9999 ");  
  20.     }  
  21.     return response;  
  22. }  
The Cyclomatic Complexity of the above function is 7. It is measured at every point that I highlighted using the RED-tangle below. Basically, you should be writing so many test cases to test this function and have 100% code coverage
 
Cognitive Complexity Vs Cyclomatic Complexity
 
For Cognitive Complexity, the measuring algorithm would be slightly different,
 
Cognitive Complexity Vs Cyclomatic Complexity
 
For every condition, it increases the count by 1, if there is a break in the condition flow i.e. combination of && and ||, it further increases it. If there are 5 conditions all gated with AND, then the score will be 1, whereas if there is combination of AND and OR, then for every break, the score will increase. 
 
So what can we do to reduce the score? If you see, there are repeated operations of division and based on the value of the result, condition is determined. So if we can create one common function of CheckDivisiblity(), it would reduce the inline condition checks.
  1. private bool CheckDivisibility(int year, int divBy, bool cont)  
  2. {  
  3.     bool response = false;  
  4.     //the value of cont is used to determine whether the previous condition is TRUE.  
  5.     //perform the current divisibility check only if the last condition is passed.  
  6.     //What we have done here is instead of making the check in the calling function we have moved the  
  7.     //check in this function and made this more like a single responsibility function.  
  8.     if (cont)  
  9.     {  
  10.         int rem = year % divBy;  
  11.         if (rem == 0)  
  12.             response = true;  
  13.     }  
  14.     return response;  
  15. }  
And now the following changes are made in the calling function. I am creating a new version of the calling function in order to summarize the score at the end of this bog,
  1. public bool IsLeapYear2(int y)  
  2. {  
  3.     bool response = false;  
  4.     if ((y >= 0) && (y <= 9999))  
  5.     {  
  6.         bool _check4 = CheckDivisibility(y, 4, true);  
  7.         bool _check100 = CheckDivisibility(y, 100, _check4);  
  8.         bool _check400 = CheckDivisibility(y, 400, _check100);  
  9.   
  10.         response = (_check4 && !(_check100)) || (_check400) ;  
  11.   
  12.     }  
  13.     else  
  14.     {  
  15.         response = false;  
  16.         throw new ArgumentOutOfRangeException("year"" The argument value should be inside 0 and 9999 ");  
  17.     }  
  18.     return response;  
  19. }  
It will now reduce both the Cyclomatic Complexity (RED-tangle) and Cognitive Complexity (GREEN-tangle)
 
Cognitive Complexity Vs Cyclomatic Complexity
 
You can further reduce the cyclomatic complexity by moving the input parameter check to a separate function,
  1. public bool IsLeapYear3(int y)  
  2. {  
  3.     bool response = false;  
  4.   
  5.     bool _validinput = ValidateInput(y);    
  6.     if (_validinput == falsethrow new ArgumentOutOfRangeException("year"" The argument value should be inside 1000 and 9999 ");    

  7.     bool _check4 = CheckDivisibility(y, 4, _validinput);  
  8.     bool _check100 = CheckDivisibility(y, 100, _check4);  
  9.     bool _check400 = CheckDivisibility(y, 400, _check100);    
  10.     response = (_check4 && !(_check100)) || (_check400);  
  11.     return response;  
  12. }  
  13.   
  14. private bool ValidateInput(int y)  
  15. {  
  16.     //validate the input parameter  
  17.     if ((y >= 1000) && (y <= 9999))  
  18.         return true;  
  19.     else return false;  
  20. }  
  21.   
  22. private bool CheckDivisibility(int year, int divBy, bool cont)  
  23. {  
  24.     bool response = false;  
  25.     //the value of cont is used to determine whether the previous condition is TRUE.  
  26.     //perform the current divisibility check only if the last condition is passed.  
  27.     //What we have done here is instead of making the check in the calling function we have moved the  
  28.     //check in this function and made this more like a single responsibility function.  
  29.     if (cont)  
  30.     {  
  31.         int rem = year % divBy;  
  32.         if (rem == 0)  
  33.             response = true;  
  34.     }  
  35.     return response;  
  36. }  
Here is the reading from the FxCop Code Analyzer for the above variants of the code. I could not present the Cognitive Complexity reading from SonarQube extension as it is set with my official credentials and could not share snapshots due to IP clauses. In the final version IsLeapYear3(), - we got rid of nested If's, however the sequence of && and || condition in creating the value for final response will still remain. It will still be part of Cognitive Complexity counts.
 
Cognitive Complexity Vs Cyclomatic Complexity
 

Conclusion

  1. Consider writing smaller methods sticking to the basics of single responsibility/method and inheritance.
  2. Take advantage of generics, common utility methods etc.
  3. Writing smaller function responsible for single job also enables you to explore calling them in parallel. The above example may not fit that bill as divisibility check by 100 and 400 does not make sense if it is not divisible by 4. Similarly, divisibility by 400 does not make sense if it is not divisible by 100. However, there can be practical scenarios where these could be run in parallel and cummulated at the calling method. Calling it in parallel has nothing to do with complexity, but just improving performance where response time is critical SLA.
Note
All my future code snippets will comply with lower Cyclomatic and cognitive complexity to the best of my coding prowess. Till then -
stay safe, stay blessed !
 
Thank You!