What is cyclomatic complexity and why you should care

What is cyclomatic complexity and why you should care

Many times, when looking at applications we have written or that we see around in repositories throughout the internet, we get this feeling that whatever we’re reading has a certain complexity too it. Sometimes this complexity is manegeable and expected. Sometime it’s not. Actually, very frequently, code can be difficult to understand and we usually describe such code as being overcomplicated, for example.

But it’s not just our understanding of written code that suffers. As expected, if code is more complex, it will also require more effort on the part of the computer to execute. And while efficient code might not necessarily be easy to understand, there is indeed such a possibility as code that is hard for humans to read and hard for hardware to compute.

However, in order to try to make things simpler and, especially, in order to coordinate within teams what is acceptable and what isn’t in terms of complexity, we need something that can help us measure complexity.

There are, of course, many metrics that can be used. In this article we’ll talk about cyclomatic complexity.

What is cyclomatic complexity?

Formally, it is the quantitave measure of the number of unique execution paths through a program’s source code, and, because of this it measures the amount of decision logic in the program. The more decision logic some piece of code has, the more complex it is.

There are a few ways to measure cyclomatic complexity. Usually, however, the control-flow graph of the program is used. A node in this graph corresponds to indivisible groups of commands and a directed edge connects two nodes if the second might be immediately excuted after the first. Also, the entry and exit points count as nodes. Finally, groups of nodes and edges might form components, where a component is defined as being a connected subset of the graph not connected to any other subset.

As an example, take the following method:

def do_stuff
  x = 1 + 1
  y = x * 10
  x + y
end

We have an entry and exit points plus 3 statements. However, these 3 statements necessarily execute in sequence, therefore they make up an indivisible group of commands, and thus all three constitute 1 node only. Therefore, the control flow graph for this method has 3 nodes (entry point, the instructions and the exit point) and 2 edges.

In mathematical terms, the formula to measure cyclomatic complexity will be:

M = E - N + 2P

Where:

  • E is the number of edges in the graph
  • N is the number of nodes
  • P is the number of connected components

In particular, for a single method, P is always one, and so we have:

M = E - N + 2

However, there’s a more practical way to calculate this: the number of if statements plus 1.

Say we have a method like so:

def do_math(number) (1)
  x = 1 + number
  y = x + 10        (2)
  x + y
end                 (3)

Here we assigned numbers to each node (entry and exit points count!). Therefore we have 2 edges connecting our nodes and only 1 possible path. To illustrate:

(1) ---> (2) ---> (3)

Using the formula:

M = 2 - 3 + 2 = 1

Which is precisely the number of if statements plus 1:

0 if statements + 1 = 1

However, suppose we decide we want to multiply y by 10 if x is 10. We’d write something like:

def do_math(number) (1)
  x = 1 + number    (2)
  y = x + 10
  if x == 10        (3)
    y = y * 10      (4)
  end
  x + y             (5)
end                 (6)

Now we have 2 possible execution paths, and therefore our control-flow graph becomes:

(1) ---> (2) ---> (3) ----------> (5) ---> (6)
                   \---> (4) --->/

So we have now 6 nodes and 6 edges:

M = 6 - 6 + 2 = 2

Again:

1 if statement + 1 = 2

But what if we have more than one predicate in an if statement? In that case, we count the number of predicates. It ends up being equivalent because, logically, if statements with more than one predicate are equivalent to multiple if statements. Nested, if we have &&, and sequential, if we use ||.

Let us modify our previous example a bit. We now take 2 numbers as arguments and our method merely returns their sum. However, if a is 10 and b is 10, we multiply the result by ten:

def do_math(a, b)         (1)
  result = a + b          (2)
  if a == 10 && b == 10   (3) and (4)
    result = result * 10  (5)
  end
  
  return result           (6)
end

This is equivalent to:

def do_math(a, b)          (1)
  result = a + b           (2)
  if a == 10               (3)
    if b == 10             (4)
      result = result * 10 (5)
    end
  end
  
  return result            (6)
end

And graphically:

(1) ---> (2) ---> (3) -----------------> (6)
                   \---> (4) --->/       /
                          \---> (5) --->/

Using the formula:

M = 7 - 6 + 2 = 3

Or, considering each predicate of the if statement as an if statement:

2 predicates + 1 = 3

That’s very cute but… what do all of these numbers mean?

Although some may think otherwise, interpreting these numbers is where the real problem begins.

We got our measuring stick, but now we need everyone to agree on how much complexity is too much complexity.

The answer is that there is no universally accepted threshold to determine that something is too complex.

To be more precise, there is a commonly accepted interpretation, given by McCabe, the creator of this metric. However, as with most things in software development, what is “too complex” will depend on the circumstances and needs of the project and the team. With that in mind, below are the threshold values traditionally used for cyclomatic complexity:

  • 1 - 10: Simple procedure, little risk
  • 11 - 20: More complex, moderate risk
  • 21 - 50: Complex, high risk
  • 50: Untestable code, very high risk

It is worth noting that one of the main tools in the Ruby world that checks this metric, RuboCop, has a default max of 7 before warning about it. Naturally, this can be configured to whatever values seem to be best.

Managing cyclomatic complexity and final thoughts

The next natural question would be: how do we keep this under control?

Given what was discussed, our main point of focus should be eliminating as many decision points as is reasonable. Given the formulas above, if you have more than 4 or 5 control flow structures you should probably take a good look if these statements can’t be converted into separate methods.

We should avoid extremes, however. There is such a thing as needless refactorings and one should also consider that maybe their tolerable cyclomatic complexity threshold is some other value than the commonly accepted ones.

Finally, and although this may seem to invalidate the time spent explaining this metric, maybe the solution to manage this is to not manage it at all. The reason I say this here is because this is a metric to help us produce better code. Its usefulness is directly dependent on how much benefit comes out of making sure our code is within the acceptable values and there are many cases where the benefit is too small for the required effort or this can become a side effect of managing other metrics deemed more convenient such as ABC size, method length, block length, block nesting, parameters lists, etc.

So much so that one of the other main linters for ruby, standardrb, disables checks for this metric completely.

I don’t think this article is wasted however. We don’t need to use everything we come to know and understand, but knowing and understanding the available tools is very important in order to be able to make prudent choices around how to write good and maintainable software.

But maybe you just don’t have the time to deal with all of your tech debt knowing your users need all those features in your backlog. We can get rid of that tech debt for you! Send us a message! opens a new window

Get the book