Code Complexity - The Importance of Keeping Things Simple
How you can keep your code simple, and how it can help you, and the developers you work with.
June 26, 2021
Coding Practices
Code complexity is basically a term used to determine how complex a piece of code is. That’s pretty straightforward. And that’s the least complex answer I guess… But more accurately - it describes how many steps/actions/paths it needs in order to complete a task. The more steps, the more complex.
Complex code can be difficult to maintain, and difficult to read - not only for yourself, but also for other developers.
So - How can I measure it?
A common way of measuring code complexity is Cyclomatic complexity - It gives you a score based on how many paths a piece of code could potentially have when called.
During this article - I’ll be mentioning a complexity score, and for this I’ll be using the CodeMetrics VS Code extension - which uses a close approximation of Cyclomatic Complexity.
Take this code sample for example, it works, but it’s doing quite a bit don’t you think? This has a complexity score of 13.
// Code complexity: 13
const calculatePrice = (items, isMember) => {
let price = 0;
items.forEach((item) => {
switch (item.name) {
case 'apple':
price = price + 1;
break;
case 'banana':
price = price + 1;
break;
case 'strawberry':
price = price + 0.5;
break;
default:
price = price;
break
}
})
if (isMember) {
price = price - (price / 10);
}
return price;
}
calculatePrice(['apple', 'banana', 'strawberry'], true)
Here I’ll show you how you can make your own code simpler and less complex 😊.
So How Can I Make My Code Less Complex?
Just like pretty much any coding task, there are a million ways to accomplish something. And making your code simpler is no exception.
Looking back at our previous example, we can start by splitting things apart. There are two “sub-tasks” this method performs. It calculates the price, as well as calculates a discount if the user is a member or not.
So by splitting our two methods, we get this:
// Code complexity: 12
const calculateItemsPrice = (items) => {
let price = 0;
items.forEach((item) => {
switch (item.name) {
case 'apple':
price = price + 1;
break;
case 'banana':
price = price + 1;
break;
case 'strawberry':
price = price + 0.5;
break;
default:
price = price;
break
}
})
return price;
}
const applyMemberDiscount = (price, isMember) => isMember ? price - (price / 10) : price;
const calculatePrice = (items, isMember) => {
let price = calculateItemsPrice(items)
price = applyMemberDiscount(price, isMember);
return price;
}
calculatePrice(['apple', 'banana', 'strawberry'], true)
Yay 🎉! We’ve made our code less complex! By one! But we have also maybe just “moved” the issue somewhere else…
You see, splitting the code will make our calculatePrice
method easier to read, and understand - but the new calculateItemsPrice
is still pretty much the same as before 😕…
So what can we do about it? Well… CodeMetrics will tell you what the issue is, and for this case it’s clear that the switch
is the most complex part of the method. So lets tackle that!
Something which I have really come across over the past few months or so of considering code complexity - objects are your friend. And in this scenario is no exception.
// Code complexity: 3
const calculateItemsPrice = (items) => {
let price = 0;
const prices = {
apple: 1,
banana: 1,
strawberry: 0.5,
}
items.forEach((item) => price = price + prices[item]);
return price;
}
Yes, that’s right. Three. There are three “steps” to run this method. The first is the method itself, the second is the forEach
bit, and lastly the third is to return a variable. Three possible outcomes that this code would run - compare that to our initial 13 in the first code sample.
So why was that so high? Well as I mentioned earlier that it was the switch
responsible for the complexity being so high. This is because whenever the function ran - there were multiple different paths your code could take. First it would need to pick it’s path, then run the relevant path for it, and then break out of the switch
. That’s three steps per case
!
So What do I Need to do?
Simply put - ensure that each of your methods have less paths/steps it needs to take.
By adding things like an if
statement - or a loop, it’s taking another step, if there are too many steps in a method (Say six or more) - the method is doing too much, and needs to be broken down.
Theoretically yes, you could have an entire script or application just run from one method - but that method would be responsible for the whole codebase.
By identifying and taking out repeated parts of the code, you’re reducing duplicate code and complexity - making it easier to maintain. Making those methods less complex would then mean that they are easier to understand as well.
As I mentioned earlier - I’ve been using CodeMetrics for VS Code to see how complex my methods are. And the way that I work would take a large method with a high score, break them down into other methods, and lower the score.
At a minimum - the best thing you can do - is to make small methods, and use as little if/else
statements.
So… What’s the Whole Point of This?
The benefit I see of making code less complex - is a few things:
Code can be Reused More
Anyone who knows me will know if I have an opportunity to do something once, I will. And that includes writing code.
Say for example, this code has the getResult
method separate. This would allow for other methods to directly get the result instead of having to use getResultsMessage
and extract it from the string.
const getResult = (result) => result > 10 ? 'Awesome' : 'Amazing';
const getResultsMessage = (score, result) => `Alrighty! Your score is ${score} - that makes you ${getResult(result)}!`
It also means that we wouldn’t be adding duplicate code, as we would be calling either method elsewhere.
It’s Easier to Maintain
So, let’s go back a second to our fruit prices example - if we were to add another fruit to the calculator, all we would need to do is add a single row into our prices
object. The code will handle the rest.
Whereas previously - we’d need to add in other case
statement - which span multiple lines - this also leads on nicely to my next point:
It’s Easer to Read
Now, reading code in your head, you would probably be reading it in English (Or your own local language even) on what it means. Essentially a bit like pseudo code. Take this example below - how would you read it?
// Code complexity: 6
const validate = (value) => {
if (value === "Invalid Value" || value === "Also Invalid" || value === "Also Invalid As Well") {
return false
}
return true
}
“If value is equal to ‘Invalid Value’, or value is equal to ‘Also Invalid’ or value is equal to ‘Also Invalid As Well’ return false, otherwise return true” is what I’d get. That’s a bit of a mouthful somewhat…
But - we can make something which is less complex, and in my opinion, also easier to read…
const validate = (value) => ['Invalid Value', 'Also Invalid', 'Also Invalid As Well'].includes(value) ? true : false
Reading this out instead is more like “If an array of ‘Invalid Value’, ‘Also Value’ and ‘Also Invalid As Well’ includes value then return true, otherwise false”.
It Can Help Out Other People
Something which is somewhat of a knock-on effect is making it easier for other developers to get to know your code quicker. We all know that clean, simple and maintainable code is much more easier to work with - so this should really be one of the key reasons why you’d want to make your code less complex.
Final Thoughts
Making your code less complex is something you should maybe keep in the back of your mind - definitely if you’re working with other people. It’s made my code more straightforward and easier to use - and I hope it helps yours too! 😊