Understanding cyclomatic complexityUnderstanding cyclomatic complexity is vital to write clean and maintainable code
5
(2)

Are you struggling with messy, hard-to-maintain code? Cyclomatic complexity could be the missing link in your code quality toolkit. As a vital software metric, cyclomatic complexity measures the number of linearly independent paths through your code, helping you identify overly complex sections that could lead to bugs and maintenance headaches. In this post, we’ll break down what cyclomatic complexity is, why it matters, and provide practical examples to help you simplify your code and improve readability.

What is Cyclomatic Complexity?

Cyclomatic complexity is a software metric introduced by Thomas J. McCabe in 1976, designed to measure the complexity of a program’s control flow. It calculates the number of independent paths through a program’s source code, effectively gauging the complexity of the code structure. Rooted in graph theory, cyclomatic complexity represents the code as a control flow graph, where nodes correspond to code blocks and edges represent control flow between them, such as decisions and loops. By quantifying the number of decision points, cyclomatic complexity helps developers assess the risk, maintainability, and testability of their code, making it an essential tool for improving software quality and reducing technical debt.

Think of cyclomatic complexity like a pilot flying a plane. On a smooth, uneventful flight, the pilot makes fewer decisions, allowing them to focus on routine tasks and maintain steady control. However, when turbulence hits or unexpected problems arise, the pilot is suddenly faced with numerous decisions in a short amount of time, increasing the complexity of the situation. Similarly, in programming, code with fewer decision points is easier to manage and maintain, like a calm flight. But as the number of decision points grows, so does the complexity, making the code more difficult to navigate and prone to errors, much like a pilot managing an increasingly chaotic flight.

What adds cyclomatic complexity to our programs?

So, what adds cyclomatic complexity to our programs? I think the best way to explain it is to start from a practical example. So, here’s a very simple method. It’s written in C#, but I think any sofware engineer would understand what it does.

It’s a very basic routine that returns a discount rate based on a few conditions. If the user is a senior, we want to give them a 25% discount. If it’s not a senior, but it’s a loyal user we still want to offer some discount, in our case 20%. If the user is neiter a senior nor a loyal user, we won’t offer any type of discount. This method is not too complicated. Yet it has a certain associated cyclomatic complexity because of all the different decisions the program needs to make. These decision points ultimately translate to different execution paths.

Baring this in mind, we can extrapolate a little bit and conclude that every line of code that implies a decision point or a new execution path adds cyclomatic complexity. Therefore, it’s all about control flow, so we add cyclomatic complexity whenever we use:

  • Loops (any type of loops)
  • If-else constructs
  • Switch statements

How do we calculate cyclomatic complexity

Cyclomatic complexity is calculated by representing the code as a control flow graph, where nodes correspond to code blocks (like sequences of statements), and edges represent the flow of control between these blocks (like branches from conditional statements or loops). The basic formula to calculate cyclomatic complexity is:

C = E− N+2P

where:

  • E is the number of edges (transitions between code blocks),
  • N is the number of nodes (code blocks),
  • P is the number of connected components or exit points (usually 1 for a single program).

Coming back to our previous code sample, here’s a flow chart that represents the execution of the routine:

We can notice that we have 8 edges (the yellow connectors) and 7 nodes (the blue boxes). There are no other connected components to this function so the P would be 1. Having all this information we can calculate the cyclomatic complexity of our routine as follows:

8 (edges) – 7 (nodes) + 2 * 1 = 1 + 2 = 3

Therefore, the cyclomatic complexity of our routine is 3. That’s not really a bad number (we’ll come to this later), but let’s say that we want to try to reduce the cyclomatic complexity even more. How do we do that?

Lowering cyclomatic complexity through refactoring

There’s obviously no one size fits all response here. The core principle we need to follow if we want to reduce cyclomatic complexity is to try to remove as many of the control flow structures from our routine. This could imply extracting to helper function, using other programming constructs or features offered by any programming language, applying design patterns and so on.

A simple way to reduce the cyclomatic complexity of our routine would be to get rid of all the if and elses. But how can we do that? Well, we could use a dictionary in which we’ll add all the possible outcomes and then we can simply try to get the dictionary element based on the combination of age and loyalty. That would look something like this:

Now the cyclomatic complexity is 1 instead of 3.

Why is cyclomatic complexity important?

There are many reasons! For me, the most important one is that the cyclomatic complexity score of any given routine gives me a clear indication on how many unit tests I have to write. Going back to the initial method I started with, we remember that the cyclomatic complexity was 3. This means that I need to write at least 3 unit tests to have all possible execution paths covered.

Having this as a baseline, we can make sure that our applications are more robust and we can better gauge the associated risks of a certain deployment. While refactoring to lower the cyclomatic complexity score of a given routine we’ll also make our code easie to read and maintain.

But, I’m sure it’s a little bit too abstract. So let’s go into a practical example from a project.

Practical example

Executing code metrics on an existing application, my attention was drawn to the following fact:

In the UpdatePersonalDetails class we have the OnParametersSet method that has a cyclomatic complexity of 12. All other methods or class members produce a cyclomatic complexity of 2. This is for me a red flag that I need to take a deeper look at the OnParametersSet method.

So, here’s the method:

There’s nothing majorly wrong with the method but there are a lot of decision points inside of it. I have highlighted them with yellow. That’s driving the cyclomatic complexity to be fairly high.

By the way, you might ask yourself what is a high value for cyclomatic complexity and what’s normal? That’s a very good question, but without a definitive answer, as a lot of things might depend on the context. But, generally you can take 10 as a decent value for cyclomatic complexity. Any score that exceeds 10 might need some further investigation. Everything below can be considered as good. Still, the most important thing, in my opinion, is to always evaluate it agains the cyclomatic complexity score of other routines in the same class. Like we’ve seen in my example, there was one method standing out from the crowd with its score.

Coming back to our task of reducing the cyclomatic complexity, we could extract some initialization logic from the OnParametersSet method in another helper method. The result would be something like this:

After this refactoring the cyclomatic complexity of the OnParametersSet method is 1. However, we now have a new method who’s cyclomatic complexity is 11. That’s the point where I’d further look into how I used nullable reference types in this class, because something might not be okay.

Some conclusions

While we went on to refactor the method with higher cyclomatic complexity, notice that we didn’t do anything else than following some standard refactoring and clean code practices. That’s why I think that taking a look at this metric is useful to understand code structures that are in need of some refactoring. In the end, this will make our code cleaner and easier to maintain.

Another key point is unit tests. You can take the cyclomatic complexity score as a guideline for the minimum number of unit tests that you need to write in order to have good code coverage. It’s true, IDEs often show you execution paths that are not covered, but sometimes they may not.

In summary, cyclomatic complexity is a valuable metric for assessing the intricacy of your code’s control flow, helping you identify areas that might be prone to errors or challenging to maintain. By understanding and managing cyclomatic complexity, you can write cleaner, more efficient code that’s easier to test and debug. Whether you’re optimizing existing code or writing new programs, keeping complexity in check is key to ensuring your software remains robust, maintainable, and scalable.

If you want to take a deeper look at this topic, here’s a useful video:

How useful was this post?

Click on a star to rate it!

Average rating 5 / 5. Vote count: 2

No votes so far! Be the first to rate this post.

We are sorry that this post was not useful for you!

Let us improve this post!

Tell us how we can improve this post?

By Dan Patrascu

I solve business problems through quality code, smart architecture, clever design and cloud sorcery. Over more than 8 years I've worked and led projects of all shapes and sizes: from regular monoliths to complex microservices and everything in between. I'm also running the Codewrinkles channel and a newsletter.