The dreaded cyclomatic complexity

The dreaded cyclomatic complexity

One of my recent hobbies has been to look at and comment on snippets that some developers post here (or on other platforms). Inspired by this, I decided to go back to writing technical articles (this time in English). This article is part of a series (I wonder how long it will be...) in which I want to share how I like to write and review code.


I won't be naive and say that this topic is simple. Most developers do not know what this means (and among those who do, many are reluctant to implement it). Still, I guarantee this is a must-have best practice to increase any codebase's testability, readability and maintainability.

But after all, what is this cyclomatic complexity?

Cyclomatic complexity is a metric used to evaluate the complexity of a codebase by counting the number of possible paths through a program.

Take a look at the code below:

Article content
code example with a complexity of 3

In this snippet, there are only two possible outputs (since it's a boolean: true or false). However, this function has three potential paths:

  1. If the wordLanguage variable is equal to en, it will enter the first condition and return true.
  2. If the wordLanguage variable is not equal to en, it will skip the first condition and return false.
  3. If any error happens, it will be caught by your try/catch statement and return false.

Each conditional statement (like if, else, for, while, switch, and even try/catch) introduces a new path, increasing the code's complexity.

As you probably realised, the higher the cyclomatic complexity, the more decision points exist in your code, making it harder to test and understand. So, the main question is:

How can complexity be reduced?

There are a few strategies that can be implemented to reduce code complexity, and I suggest applying them in the following order:


1. Guard clauses

Begin with the Guard clauses technique. This tends to simplify your code, and yes, it can reduce code complexity. However, this will most likely not work in practice 😔. Here is where most developers get frustrated working on this topic.

Even if your if/else conditionals are reduced (which is super), there's no guarantee that the possible paths will also be reduced.

Article content
the code on the left has a complexity of 4 without implementing the guard clauses technique, and on the right, we have it implemented, and the complexity is still 4


2. Modularise

Breaking down large functions into smaller ones is often practical since complexity is measured per function. The rule of thumb is split based on the Single Responsibility Principle (SRP), which suggests that each function should perform only one task.

In the isEnglishWord mentioned before, this function performs three tasks: first, it detects the language, then it compares if the detected language is English, and on top of this, it handles the errors returning a default value. So the strategy to implement is to isolate the responsibilities for the tasks that increase complexity, for example, the "evaluation" task, as the example below:

Article content
code example with a complexity of 2

With this approach, the function isEnglishWord has two possible paths: isEn or false (in the error handling), and the isEn function has two possible paths: true or false. The code complexity is 2 in both functions, so the max complexity of this code is 2.


3. Leverage Array and Object Methods

If you're using traditional for or while loops, replacing them with functional programming methods like filter, map, and reduce will undoubtedly reduce the complexity because (as already said) they are measured in the function context. So I'll skip further explanations about it.

However, eventually, we can have tricky situations that are not in the scope of an array or object (yet), for example, when we have many if/else conditions or switch statements:

Article content
Switch case adds 1 to the complexity for each case

So, in the snippet above, there are six possible paths (complexity of 6), and breaking this down into seven other functions (by nesting the next if conditions) is not a good approach.

So how do we solve it?

The simplest approach

The most straightforward approach is to map the possible responses in an object and use this object keys to access:

Article content
Implementation of "object map" approach

With the approach above, we reduced code complexity to 2; even with many possible paths, the complexity remains the same (returning one of the values from LanguageMap or the language in the catch block).

The downside of this approach is handling cases not mapped in the object. These must be addressed in the catch block (as in the example) or avoided by first validating whether the wordLanguage value is a key in LanguageMap.


The most versatile approach

The most versatile approach is to create an array of objects instead; the advantage over the simple approach is that now you can have custom logic in each object and then use the find method to match a case:

Article content
Implementation of "array of object" approach

The downside is the verbosity it can cause; if the list grows too large, code readability may be affected, making it necessary to follow a schema for your options object. From a performance perspective, the time complexity of the array find method is O(n) (where n is the number of array elements). In the worst-case scenario, the method will iterate through the entire list.


Conclusion

Initially, it's not easy, but it is possible to control code complexity by rethinking how you code and following the formula proposed in this article. I'm currently working with a max complexity of 2; after some time, you get used to having readable and modularised code.

I once worked on a project with a maximum complexity exceeding 40; the functions were incredibly difficult to manage and test. I don't think all codes should have a maximum complexity of 2, but reviewing and maintaining a 10+ complexity code is exhausting, so try to keep the complexity at a maximum of 4.

Tip: ESLint has the plugin eslint-plugin-complexity to track cyclomatic complexity by setting a maximum limit.

Love this! Glad you missed out my technique of hitting it with a spaghetti code hammer

To view or add a comment, sign in

More articles by Danilo Pereira

Explore content categories