As software projects evolve, maintaining a healthy codebase can become increasingly challenging. Code can become hard to manage when there aren’t any guidelines to keep it clean. We’ll explore some practices that apply to any programming language so you can keep an organized codebase with the most up-to-date technology trends.
This article will cover the top steps toward maintaining a healthy codebase:
- Meaningful Naming Conventions
- Code Readability
- Testing Your Code
Meaningful Naming Conventions
Naming is one of those things that is harder than it should be, I’m sure you’ve been there. It is very important to choose consistent and meaningful names to improve readability when looking back at old code and when collaborating with others. There are many guides out there to set these conventions, but I took inspiration from CodeAesthetic, and below are some of my favorite points:
- Avoid variables with single letters: they don’t really tell you anything about the variable and are hard to understand
- Avoid abbreviations: abbreviations rely on the context you may or may not have; avoiding them prevents spending unnecessary time trying to understand old code
- Write units in variable names when applicable: variables like delay can be improved by specifying the unit (delaySeconds, delayMinutes, etc.)
Avoid Deeply Nested Code
We say code is deeply nested when internal logic, often conditionals and loops, lead to three or more levels of indentation. Nested code can affect overall readability, often leading to undesired bugs and making refactoring substantially harder. Let’s take the following example as a case study:
function computeResult(num1, num2) {
if (num1 > num2) {
let result = 0
for (let low = num2; num2 < num1; low++) {
let squaredNum = low ** 2
if (squaredNum % 2 === 0) {
result += squaredNum
}
}
return result
} else {
return num2 - num1
}
}
Can you tell me what this function does? How could we better structure this code?
There are two techniques to de-nest code inspired by CodeAesthetic I like to use: Extraction and Inversion.
Extraction
Extraction is when you pull out code into its own function. We can extract the logic inside the for loop into its own function in the code above.
function filterNumber(number) {
let squaredNum = number ** 2
if (squaredNum % 2 === 0) {
return number
}
return 0
}
function computeResult(num1, num2) {
if (num1 > num2) {
let result = 0
for (let low = num2; num2 < num1; low++) {
result += filterNumber(low)
}
return result
} else {
return num2 - num1
}
}
Now we can easily understand the logic inside the for loop is to filter a given number. This is very useful because now a specific piece of code has gained a specific meaning through a new function, filterNumber, and the main code has become easier to understand. Let’s see what else we can do.
Inversion
Inversion is when you flip conditions to promote an early return. This usually involves writing the unhappy paths higher in your code block, leading to early returns.
function filterNumber(number) {
let squaredNum = number ** 2
if (squaredNum % 2 === 0) {
return number
}
return 0
}
function computeResult(num1, num2) {
if (num1 < num2) {
return num2 - num1
}
let result = 0
for (let low = num2; num2 < num1; low++) {
result += filterNumber(low)
}
return result
}
By rearranging the conditionals, we lead to an early return and get rid of the ‘else’ statement, cleaning up your code. Code functionality remains the same, yet the function is and reads much better.
These practices help you write more modular and DRY code. A good set of guidelines to abide by are:
- Keep your conditional blocks short: This helps increase readability when looking back at old code
- Consider refactoring code that is more than 2-3 levels deep: This makes code more understandable and easier to follow
- Move nested logic into its own separate function: This gives a name to the action you’re trying to do
Test Your Code
Testing is another crucial component in software development. Not only does it help catch bugs earlier during development, but also helps build confidence in your codebase. A widely used approach is Test Driven Development (TDD), where you start coding your unit tests based on business requirements, forging the logical sequence your code should follow. We’re firm believers in writing tests, and in fact, we have testing tenets we abide by.
If that seems too advanced, don’t worry, we’ve got you covered. Check this guide on writing a testing plan that scales with your codebase.