While drinking my morning brew, I came across an interesting topic: Cyclomatic complexity. At first, it was a bit confusing, but then it clicked. Cyclomatic complexity is all about simplifying decision-making in your code.
The more decisions your code makes—through if
statements, loops
, or conditions
—the more complex it gets. Each decision creates a new path, making the code harder to read, maintain, and debug. Cyclomatic complexity measures how many paths your code can take.
Think of it like driving: the more roads you have to choose from at every turn, the easier it is to get lost. In code, each decision point is like another road, and too many roads make it harder to understand and maintain your code.
High Cyclomatic Complexity Leads to Spaghetti Code
As complexity increases, your code can quickly become tangled and hard to follow. This is what we call spaghetti code—code that’s messy, hard to read, and even harder to maintain.
When code is hard to understand, it’s a massive productivity killer. Developers waste time trying to figure out what’s going on instead of adding new features or fixing bugs. You might end up with redundant computations, inefficient loops, or unnecessary API calls that are easy to overlook.
Spaghetti code makes everything slower—debugging, extending, and collaborating. It drags down the team’s productivity and overall progress.
Let’s look at how we can make our TypeScript code easier to follow by reducing cyclomatic complexity. I’ll show you practical examples and explain why they work.
I’m using TypeScript because it’s widely adopted and easy to read, but you can easily apply these concepts to whichever language you prefer.
Break Large Functions into Smaller Pieces
Functions with lots of decision points (if
statements, loops
) can get complicated fast. Breaking them into smaller, focused functions makes the code easier to read and manage.
Here’s an example of a function with multiple nested if
conditions. It’s like having to make multiple driving decisions at every turn, adding complexity to the function.
Breaking large functions into smaller ones improves readability and reduces mental load. Each smaller function becomes more reusable and testable because it does just one specific thing.
Instead of wrapping the order processing logic inside multiple if
statements, we use a guard clause to return early if the order isn’t valid. This flattens the function, improves readability, and reduces the number of nested decision points.
By moving the logic into canProcessOrder()
, we reduce the complexity of processOrder()
and make it easier to follow at a glance.
Replace Complex Conditionals with Strategy Pattern
When a function has too many if
or switch
cases, things get complicated quickly. Each additional case adds another path the code can take, which increases cyclomatic complexity.
This function has five possible paths, each leading to a different outcome. It’s like deciding between five roads, making the logic harder to follow and maintain. Every time a new discount type is added, the main logic gets more complex and harder to manage.
Every time you add a new discount type, you modify the main logic, which could introduce errors or make the function even harder to maintain.
Instead of continuing to add more if-else cases, we can use an object lookup or the Strategy Pattern to simplify the logic.
Strategy Pattern
When branching logic grows, it becomes harder to read and understand. Each additional branch requires more mental effort to trace. By using the strategy pattern, the code is cleaner and easier to follow because it reduces the need to manually trace through multiple if
statements.
By using an object to store strategies, we remove the branching logic from the main function. It’s now easier to add or update discount types.
Avoid Too Many Parameters (Above 5)
This is one of my favorite rules to follow. Functions with too many parameters are tough to work with and to maintain. It’s a common mistake for beginner to intermediate developers.
Grouping related parameters into an object simplifies function signatures and reduces complexity.
By grouping the parameters into a UserInfo
object, the function is now easier to work with, especially if more fields need to be added in the future.
Union Types
Union types in TypeScript reduce the need for multiple checks and simplify your code. A union type in TypeScript allows a variable to hold one of several types. In this case, input
can be either a string
or a number
.
This implementation does a runtime check with typeof
to determine whether the input is a string
or a number
, and executes different logic based on the result.
Instead of directly writing string | number
in the function, let’s create a type called Input
to represent it. Then, we use that Input
type in the function.
TypeScript knows that input
is either a string
or number
, so we simplify the logic and let TypeScript handle the checks. This version combines both types into one simple check and prints the type directly, treating both types the same.
Avoid Deeply Nested Loops
Deeply nested loops increase cyclomatic complexity because every additional loop adds more decision points. This can make code harder to read and maintain. A good way to simplify is by using helper functions to separate the logic, reducing the nesting and improving clarity.
We have two nested loops: one to iterate through the outer array and another to go through each innerArray
. In my opinion, nesting like this is unnecessary complexity.
By refactoring the nested loops and moving the innerArray
logic into a helper function, we improve the structure of the code. Let’s break it down:
-
Reduce nesting: The outer loop no longer has a second loop inside it, making the flow much easier to follow. Fewer nested loops mean less mental overhead when reading the code.
-
Improve readability: Each function does one thing:
handleInnerArray
processes the inner arrays, while the main logic just deals with the outer structure. This separation of concerns makes the intent of each piece clearer and easier to maintain. -
Reusability: The
handleInnerArray
function can be reused wherever we need to process arrays in the same way, avoiding code duplication and making future updates simpler.
This approach helps reduce cognitive load by breaking complex logic into smaller, manageable pieces, and it also reduces cyclomatic complexity, which keeps the code more maintainable in the long run.
Cyclomatic complexity is all about choices. Just like taking too many turns on unfamiliar roads can get you lost, complex code paths can lead to confusion, bugs, and difficult maintenance.