Easing into Cyclomatic Complexity

igneel64

Peter Perlepes

Posted on April 5, 2020

Easing into Cyclomatic Complexity

Most people reading this article might have or will be in the situation of looking at a project's code and scratching their heads why they might not be able to reason about its outputs. This article is probably gonna help you at least understand why you might have this difficulty for certain modules. You are not alone in this.

The reality

function makeInitialState(x, y){
  const state = [];
  if(x.a || y.b){
    state.push(x.b && y);
  }
  return state;
}

The above is a generalized view of some code that you will encounter in the wild OSS community or on the project you are maintaining day to day.

Stand back for a second and imagine x and y being two known entities passed around your program. They are familiar, like bookings, hotels, shirts, todos or anything you are familiar with.
Even in that scenario you will not be able to so easily reason about when the output or side effect will be different.

Maybe because its cyclomatic complexity is 4...

Intro to our program flow

You can skip it if you feel like it

One of the most important things that changed my view on reading but also on writing programs is coming to terms with Cyclomatic Complexity. The first thing not to do is being daunted by the term.
It is a software metric that was defined back in the old days of 1976 by Thomas J. McCabe, Sr. and has been studied across the years, at some points also applied to official security standards like ISO and IEC.

When writing a program, we start with an empty abstraction, either that be a module, a class or a function. Going into this new abstraction, we define the things we wish to happen at the point this code is executed. You can think of these statements as points in the path your machine will run when you code executes.

   ○       // Entry
   ↓
   ⬢       // Statement
   ↓
   ●       // Exit

There is only one path our code can take...

This can be considered the flow of our statements.

At some point, due to requirements or initial definition of our program, we have to add some logic that will:

  • Loop through some statements (while, for)
  • Decide if statements should be run or not (if, else, switch)
  • Evaluate if the program should throw an exception and stop in its tracks (try-catch-finally)
  • Branch out of the current execution (break, continue)

The simple conditional statement will change our path to something like

function makeConditionalState(x){
  const state = createEmptyState();
  if(x){
    state.push(x);
  }
  return state;
}

And with weird symbols:

   ○       // Entry
   ↓
   ⬢       // StatementA -> Always executes
   ↓
   ⬢       // Conditional
   |  ↘    
   |    ⬢  // If conditional is true execute StatementB
   ↓  ↙
   ⬢       // Exit conditional
   ↓
   ●       // Exit

There are two possible paths our code can take based on the flow...

The above (when created in a correct way) is called a Control flow graph and helps us visualize the flow of our program as a graph.

Into the Complexity

By adding more conditionals or other control flow statements to our abstractions, the paths of execution that our code might take naturally increases.
As humans with minds that can hold up finite amounts of information at any point in time, it becomes much more difficult to reason about the expected outputs of a function when there are many paths the flow can take.

In simple terms this is Cyclomatic Complexity. The independent flow paths that our abstraction can take while executing.

Let's look at some JavaScript examples next, but the same terms apply to most programmings languages we use these days.

Some examples

Let's start with the scenario that we are working on an ecommerce store and we are creating the function to calculate and return the price of a product based on some current state.

/* Warmup */
function getPrice(currentState){
   const results = calculatePrice(currentState);
   return results;
}

Cyclomatic Complexity : 1
Pretty simple, one path function. No conditional logic, so no additional paths to be generated

/* Single conditional */
function getPrice(currentState){
   if(currentState.loaded){
      return calculatePrice(currentState);
   }
   return 0;
}

Cyclomatic Complexity : 2
Single conditional logic. Now depending the application state being loaded we return an actual result or 0. So one path for the case of loaded being true and one more path for the case of loaded being false.

Now we are asked to return also 0 when the customer is a guest, so the "first thing" would be to go with something like:

/* Single conditional, two conditions */
function getPrice(currentState){
   if(currentState.loaded && !currentState.isGuestSession){
      return calculatePrice(currentState);
   }
   return 0;
}

Cyclomatic Complexity : 3
Now this starts to get a bit more complex. You might be wondering why this results to Cyclomatic Complexity of 3, even if this function has only two possible outputs and a single conditional statement.

Unwrapping the above code we can see that the && operator can be also interpreted in this case as:

/* Multiple conditionals, single condition */
function getPrice(currentState){
   if(currentState.loaded){
     if(!currentState.isGuestSession){
      return calculatePrice(currentState);
     }
   }
   return 0;
}

Now you may have a clearer picture of the "possible paths" that the execution can take and lead up to 3 unique.

*Remember that in order to reason about the output of this function you have to keep in your head 1) If the state is loaded, 2) If the session is that of a guest and 3) What this function is possible to return. Pretty much for a busy person if you ask me.

Verdict

I hope you are starting to get a rough understanding why increased Cyclomatic Complexity might make it harder for software engineers to reason about their code and expected outputs.
In my experience, when encountering code with relatively high Cyclomatic Complexity, there are many more things going on under the covers:

  • Low test coverage
  • Absence of Design Patterns
  • "Speed over Quality" project conditions

Feeling better

Congratulations on making it this far! I am pretty sure you might have learned something new or at least refreshed your understanding on Cyclomatic Complexity.

Calculating the Cyclomatic Complexity of an abstraction might be nice for practise in simple functions but our day to day interaction probably have to do with much more complicated constructs. Trying to figure out each functions Cyclomatic Complexity by going over it one by one, sounds a daunting task and not so much "time-well spent". But there are some steps you can take and make your life much easier!

Now what ?

Another really surprising fact that I learned as I was researching this topic, was that one of the most used linting tools for JavaScript, ESLint, has a Cyclomatic Complexity rule by default!

By adding the rule in your ESLint configuration file, you can now inspect and report the Cyclomatic Complexity of functions on your codebase.
Please start with something lax like 8-9 and start lowering as you go.

  "rules": {
    // ...
    "complexity": [2, 8]
  }

Now every function that has a complexity more than 8 will be reported by the linter. Even better, if you text editor has a plugin or integration with ESLint (like vscode-eslint), you can now see the warnings as you are navigating over your files.
ESLint warning on code

Some more tools

As our ability to reason about our code and prioritizing it on the projects we are working on is one of the most commonly discussed topics among peers, there are some more tools to help you to recognize, facilitate and consistently check Cyclomatic Complexity and other metrics:

  1. complexity-report-html A library which allows you to get a report of the current state of your JavaScript codebase with metrics like Cyclomatic Complexity. (Shameless plug!)
  2. Code Climate Much more sophisticated tool with many features including code complexity analysis.
  3. CodeScene/Empear Visualization of code metrics and predictive analysis.

For any comments, feedback or just something you want to discuss, you can hit me up here on DEV or on any of my social platforms. Cheers!

💖 💪 🙅 🚩
igneel64
Peter Perlepes

Posted on April 5, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related