Cyclomatic complexity is a measure of how “complex” a piece of code is by counting the number of unique paths someone could take to reach the end. Another way to think of it is how many unique unit tests would I have to write for a particular method to test all possible outcomes. The numbers of tests is what we would define as our cyclomatic complexity.
Cyclomatic Complexity Examples
Nothing beats a real world code example. So let’s take a look at a few.
public bool MyMethod(int myValue) { return true; }
The above piece of code has a cyclomatic complexity of exactly 1. Because there is only one way to pass through the code, top to bottom.
public bool MyMethod(int myValue) { if(myValue % 2 == 0) { return true; } else { return false; } }
Now our code has a cyclomatic complexity of 2. Because either the number we pass in is divisible by 2, and therefore we return true, or it’s not and we return false.
public bool MyMethod(int myValue) { if(myValue < 0) { return false; } if(myValue % 2 == 0) { return true; } else { return false; } }
The above example I included intentionally to illustrate that the cyclomatic complexity does not necessary depend on the unique outcomes, but instead the path any particular execution of the code can take. In the above code, the cyclomatic complexity is 3, even though two of the paths return false.
Why Does It Matter?
The most obvious next question is, why does it matter? Or even does it matter at all? And it’s actually somewhat hard to answer. As we will talk about soon, I’m not sure there is a perfect “threshold” where we can say, no method should be above this, but at the same time, cyclomatic complexity can be a good sign that things need to be broken up into smaller parts. Certainly it’s a sign that a method is maybe complex to follow by any new developer reading the code for the first time. It’s also typically a sign that any manual or automated testing may require several different tests to pass through the same method, testing all possible pathways and outcomes.
Furthermore for unit testing, cyclomatic complexity generally leads to more test “set up” to make sure the stars align so you can test a specific outcome. The more set up you have to do for a unit test, often the more brittle a test will become and any small change can make your house of cards fall down, even if your particular test isn’t testing that scenario.
Let me give you an example piece of code :
public Person GetPerson(int personId) { if(getCurrentUserRole() != UserRole.Admin) { throw UnauthorizedException(); } var person = personRepository.GetPerson(personId); if(person == null) { throw NotFoundException(); } return person; }
If I write a unit test to check that when a person is not found, I throw a NotFoundException(), I first have to ensure that the underlying method to getCurrentUserRole() returns an admin. Even though my unit test is trying to test a specific outcome (NotFoundException), I have to set up my unit test to first handle the pre-conditions. Any pre-conditions added to this method (For example, maybe I also want to ensure that the PersonId is more than 1), then need to be accounted for in my unit test even though again, the unit test itself is not testing the pre-conditions.
While this in of itself is not an example of cyclomatic complexity (Probably more in the realms of the SOLID Principles), cyclomatic complexity adds to this because inevitably you are testing a single condition at what could be a long train of conditions before it.
Cyclomatic Complexity “Thresholds”
If you’ve ever used a static analysis tool such as SonarQube or NDepend before, you’ve probably seen cyclomatic complexity (Or it’s sibling cognitive complexity) thresholds thrown at you. The two common numbers are either 10 or 15. I don’t necessarily think that there is any particular magic number that you should stay under. But around 10 unique pathways through a method should tell you that you do need to break things up if at all possible.
Lowering Cyclomatic Complexity
The most common way I see developers try and break up large methods is actually by creating many private methods or sub routines that then get called, in sequence, from the original method. Arguably, this has not lowered the complexity of the method and if anything, has simply made it harder to understand by any reader.
The most common methods for lowering complexity are :
- Refactoring or Reordering a series of IF/Else statements to better control flow, exit early, or re-use certain if/else statements.
- Using design patterns such as the Strategy Pattern to swap implementations on the fly (For example using Dependency Injection) rather than relying on if/else statements
- Similar to the above, using factory patterns to offload logic for creating objects to a centralized place, and reducing the complexity within individual methods
- Where it makes sense, creating re-usable helpers and services that encapsulate functionality or logic away from individual private methods.
Again, most of the above is simply moving the logic away from one method into another which in some cases, does not reduce your application complexity at all. But in those cases, it can often improve the readability of your code, and make it simpler to understand for the next developer.