When we say that someone’s code quality is bad, in most of the time, what we really mean is that the code is hard to understand. Many developers prefer to start a new project from scratch rather than add features to a legacy project, because the mental cost of understanding the code is much higher than creating something new. As the features of the project increases, it inevitably becomes more difficult to understand. Therefore, it’s important to have a way to measure and control the code’s complexity and understandability.
In this article, I will introduce two mainstream methods for measuring code complexity - cyclomatic complexity and cognitive complexity. While explaining why cyclomatic complexity may not always be sufficient, I will also introduce you cognitive complexity. Using its rules, I will explain which programming behaviors can make code harder to comprehend.
Cyclomatic Complexity was initially formulated as a measurement of the “testability and maintainability” of the control flow of a module.
Cyclomatic Complexity measuring code complexity via those metrics:
Image we have a function with flow graph above, we can calculate the cyclomatic complexity is V(G) = 9(edges) - 8(nodes) + 2 = 3. So when the numbers of decision points increases, while the node numbers is not changing, We will say it is more complex in cyclomatic complexity metric.
Simply put, we can calculate cyclomatic complexity via counting the independent flow paths(decision points) that our abstraction can take while executing.
1 | function makeConditionalState(x){ |
1 | ○ // Entry |
The presence of the conditional statement if(x)
in this program creates a decision path, resulting in a cyclomatic complexity of 2.
Cyclomatic complexity is a useful metric to measure code complexity, but it has its problem. Let’s looks into two examples:
1 | function sumOfPrimes(max) { // +1 |
1 | function getWords(number) { // +1 |
While Cyclomatic Complexity gives equal weight to both the sumOfPrimes
and getWords
methods, it is apparent that sumOfPrimes
is much more complex and difficult to understand than getWords
. This illustrates that measuring understandability based solely on the paths of a program may not be sufficient.
Cognitive Complexity is a more comprehensive metric than Cyclomatic Complexity, as it measures not only the number of control flow structures, but also how they interact with each other and the mental effort required to understand the code. It assigns a cognitive weight to each control flow construct based on its complexity and interactions with others, enabling a more accurate assessment of code readability and maintainability. This is important because certain code constructs, such as nested loops and conditional statements, can be more difficult for humans to understand and reason about than others.
A Cognitive Complexity score is assessed according to three basic rules:
Additionally, a complexity score is made up of four different types of increments:
These rules and the principles behind them are further detailed in the following sections.
A guiding principle in the formulation of Cognitive Complexity has been that it should incent
good coding practices. That is, it should either ignore or discount features that make code
more readable.
Null-coalescing
1 | // bad practice |
Cognitive Complexity will ignore null-coalescing to incent good coding practices.##
Another guiding principle in the formulation of Cognitive Complexity is that structures that
break code’s normal linear flow from top to bottom, left to right require maintainers to work
harder to understand that code.
Some of them are:
Catches
1 | try { // +1 |
A try...catch
will contribute complexity very similiar to if...else
. So it also increment to cognitive complexity.
Switches
A switch
and all its cases combined incurs a single structural increment. This is different than cyclomatic complexity, which incurs increment for each case.
But for maintainer’s point of view, a switch
with cases is much easier to understand than if...else if
chain.
1 | function getAnimalSound(animal) { |
When we using switch
we only need to compare a single variable to a named set of literal values, making it easier to understand and maintain.
1 | a && b |
Understanding the first two lines isn’t very difficult. On the other hand, there is a marked difference in the effort to understand the third line.
When mixed operators, boolean expressions become more difficult to understand.
1 | if (a // +1 `if` |
Unlike Cyclomatic Complexity, Cognitive Complexity adds a fundamental increment for each
method in a recursion cycle, whether direct or indirect. Because Recursion contribute very similiar complexity like Loop.
Nesting flow-break is something that heavily increase code complexity, five if...else
nested is much harder to understand than same five linear series of if...else
.
1 | void myMethod () { |
Let’s look back to our first example, where cyclomatic complexity give them the same score.
1 | function sumOfPrimes(max) { |
1 | function getWords(number) { |
The Cognitive Complexity algorithm gives these two methods markedly different scores,
ones that are far more reflective of their relative understandability.
With Cyclomatic Complexity, it can be difficult to differentiate between a class with a large number of simple getters and setters and one that contains complex control flow, as both can have the same number of decision points. However, Cognitive Complexity addresses this limitation by not incrementing for method structure, making it easier to compare the metric values of different classes. As a result, it becomes possible to distinguish between classes with simple structures and those that contain complex control flow, enabling better identification of areas of a program that may be difficult to understand and maintain.
Cyclomatic Complexity | Code Quality | Readability | Maintainability |
---|---|---|---|
1-10 | Clear and well-structured | High | Low |
11-20 | Somewhat complex | Medium | Moderate |
21-50 | Complex | Low | Difficult |
51+ | Very complex | Poor | Very difficult |
Cognitive Complexity | Code Quality | Readability | Maintainability |
---|---|---|---|
1-5 | Simple and easy to follow | High | Easy |
6-10 | Somewhat complex | Medium | Moderate |
11-20 | Complex | Low | Difficult |
21+ | Very complex | Poor | Very difficult |
One effective way to manage complexity metrics for your code is by using ESLint, a popular linting tool that can help detect and report on various code issues, including Cyclomatic and Cognitive Complexity.
To set up ESLint to report on Cyclomatic Complexity, you can use the eslint-plugin-complexity
plugin, which provides a configurable rule for enforcing a maximum Cyclomatic Complexity threshold. First, you’ll need to install the plugin by running npm install eslint-plugin-complexity
. Then, you can add the plugin to your ESLint configuration file and configure the maximum threshold value:
1 | { |
In this example, we’ve set the maximum threshold to 10. If the Cyclomatic Complexity of a function or method exceeds this threshold, ESLint will report an error.
To set up ESLint to report on Cognitive Complexity, you can use the eslint-plugin-cognitive-complexity
plugin, which provides a configurable rule for enforcing a maximum Cognitive Complexity threshold. First, you’ll need to install the plugin by running npm install eslint-plugin-cognitive-complexity
. Then, you can add the plugin to your ESLint configuration file and configure the maximum threshold value:
1 | { |
In this example, we’ve set the maximum threshold to 15. If the Cognitive Complexity of a function or method exceeds this threshold, ESLint will report an error.
By setting up these metrics in ESLint, you can proactively monitor and manage the complexity of your code, making it easier to understand and maintain over time.
In conclusion, code complexity is a crucial factor that can significantly impact work efficiency and project maintainability. The use of Cyclomatic Complexity and Cognitive Complexity metrics can help measure and manage code complexity, allowing developers to identify potential problem areas and optimize code readability and maintainability.
There are several ways to reduce code complexity, like using design patterns and applying TDD(Testing Best Practice Tdd). Overall, by understanding and managing code complexity, developers can build better, more maintainable software that delivers value and meets user needs.
software engineering — Mar 4, 2023
Made with ❤ and at Earth.