Best Practices for Writing Clean Code

In this article:
Subscribe to our blog:

Clean code is similar to a well-written article. Good articles possess certain characteristics that make them a pleasure to read: clear and accessible language, consistent formatting, and a well-organized structure comprised of descriptive headings, subheadings, and short paragraphs that make the content skimmable. 

Your code should read like a good article. When writing code, it’s not enough to simply make it work. You should also aim to make the code readable, understandable, and easy to maintain for those who will work with it in the future. How you format your code reflects the level of thought, attention to detail, and effort you invest in creating it.

This article explores the concept of clean code, providing key principles and best practices for writing code that is not only functional but also easy to read and maintain.

What Is Clean Code?

Clean code describes computer code that is easy to read, understand, and maintain. Writing clean code entails following a set of conventions, standards, and practices that make the code more consistent and expressive. 

These range from coding-related practices such as using consistent formatting, short functions, and clear comments to implementing automated security testing strategies like SAST and DAST.

When teams adhere to clean code principles, the codebase is easier to read and navigate, making it easier for developers to get acquainted with the code and start contributing. It’s also easier to debug and maintain a clean codebase.

How To Assess Whether A Codebase Is Clean Or Not

Key indicators of a clean codebase include consistent formatting throughout the code, thorough documentation, and a well-organized folder system. Regular code reviews and a robust testing system are also good signs, as they indicate a team's commitment to improving the codebase and ensuring that the code functions as intended.

Software teams can automate code reviews and testing by installing a tool like Codacy in their workflow. Codacy performs automated security checks across code repositories and Pull Requests (PRs) to find and resolve issues relating to coding style, syntax, compatibility, and maintainability. 

Codacy also integrates code testing capabilities into Visual Studio Code and IntelliJ IDEA via IDE extensions, enabling developers and teams to write clean code from the start and address any issues as they arise in real-time.

With the right tools and adoption of clean code practices, organizations can build a codebase that follows the “FSWC” principle:

  • Focused: Each function, class, and module should have a single responsibility and execute it effectively.

  • Simple: Aside from being easy to read, clean code should be expressive and leave the reader with a clear understanding of its purpose.

  • Well-organized: Clean code extends beyond the code; the folders and files that make up the application should be organized in a way that is easy to understand.

  • Tested: Clean code is reliable—if the tests aren’t passing, it’s not clean.

So what are some of the coding practices for writing clean code? Here are our top tips to help you get going.

Best Practices For Writing Cleaner Code

When writing clean code, there is a degree of subjectivity to consider. What one developer considers good practice may not be seen the same way by another.

That said, there are general conventions we can follow to write cleaner code. Let’s explore them now.

Use Consistent Formatting And Indentation

Imagine attempting to read a book with inconsistent line spacing, poorly indented paragraphs and line breaks all over the place. That would be frustrating, right?

The same goes for your code. Maintaining consistent indentation, line breaks, and formatting all contribute to making code clear and easy to read. Below is an example of good versus bad practice.

The Good:

 

function checkUserAge(user) {
    if (user.age >= 18) {
        return `${user.name} is an adult.`;
    } else {
        return `${user.name} is not an adult.`;
    }
}

const user1 = { name: 'Alice', age: 25 };
const user2 = { name: 'Bob', age: 15 };

console.log(checkUserAge(user1)); // Alice is an adult.
console.log(checkUserAge(user2)); // Bob is not an adult.

There are several good things about this code:

  1. At a glance, you can tell that there is an if/else statement within the function.

  2. The curly braces and consistent indentation make it easy to tell where each code block starts and ends.

  3. Notice how the opening brace for the function and the if are on the same line as the parenthesis.

  4. Each object and console.log statement starts on a new line.

The Bad:

 

function checkUserAge(user) {
if(user.age>=18)
{return `${user.name} is an adult.`;}else{return `${user.name} is not an adult.`;}}const user1={name:'Alice',age:25};const user2={name:'Bob',age:15};console.log(checkUserAge(user1));console.log(checkUserAge(user2));

A lot is wrong with this code:

  1. The indentation is all over the place — you can’t tell where the function ends, or where the if/else block starts 

  2. The line spacing braces are confusing and inconsistent

  3. The code is messy and untidy, making it hard to read

Although this example is slightly exaggerated, it shows the importance of consistent indentation and formatting. Most people would agree that the first example is much easier to understand than the second one. 

Manually formatting your code can be tedious, especially when working with codebases in multiple programming languages.

With Codacy, you can run language-specific lint tools to check source code for programmatic and stylistic errors automatically. It supports all major programming and styling languages, including Python (PyLint), JavaScript (ESLint), PHP CodeSniffer (PHP), CSS (CSSLint), and SCSSLint (SASS).

 codacy linters

This feature extends to your IDE as well via extensions. 

codacy IDE extension

Use Clear Variable And Method Names

In simple terms, variables store data and methods perform actions on (and with) the data. Always give your variables, functions, and methods descriptive names, as it makes it easier for others (and your future self) to understand the purpose and behavior of your code.

For example, here’s code with clear and descriptive names:

 

function calculateTotalPrice(items) {
    let total = 0;

    items.forEach(item => {
        total += item.price;
    });

    return total;
}

const shoppingCart = [
    { name: 'Apple', price: 1.00 },
    { name: 'Banana', price: 0.50 },
    { name: 'Orange', price: 0.75 }
];

const totalPrice = calculateTotalPrice(shoppingCart);
console.log(totalPrice); // Outputs: 2.25

This code above is clean for several reasons:

  1. The function is named based on what it does and the argument is also descriptive. At first glance, a developer reading the code can tell that when they call calculateTotalPrice() with an items array, they get back the total amount. 

  2. The object and its property names are also descriptive, so anyone reading the code knows what kind of value to expect

Now compare it to this:

 

function calc(x) {
    let t = 0;

    x.forEach(y => {
        t += y.p;
    });

    return t;
}

const cart = [
    { n: 'Apple', p: 1.00 },
    { n: 'Banana', p: 0.50 },
    { n: 'Orange', p: 0.75 }
];

const result = calc(cart);
console.log(result); // Outputs: 2.25

This code has a couple of issues:

  1. Weak function name: The function name calc is vague and does not specify what it calculates, leading to confusion.
  2. Unclear variable names: Variables like x, t, and y lack meaningful context; it doesn’t hint at the type of value it holds.
  3. Poor readability: The overall lack of descriptive names forces the reader to decipher the code, which can lead to misunderstandings and an increased likelihood of errors.

Choosing the right names becomes even more imperative as your app grows. The most important thing is to be consistent with your naming style. For example, you can use either camelCase or under_scores, but not both.

Always remember to name your functions, methods, and variables by what that thing does, or what that thing is. If your method “gets” something, add “get” in the name (e.g., getStudentId). If your variable stores the description of a product, call it productDescription, for example.

The final tip is to create single-responsibility functions, as it makes naming much easier. For example, instead of getAndSaveUser(), break it into two smaller functions: getUser() and saveUser().

Use Comments Where Necessary

Good code should be complemented with comments where necessary. Think of comments as little notes you write to your future self or anyone reading your code, explaining parts of the code that may not be obvious.  

Comments shouldn’t be used to teach beginners what the code does. For instance, there’s no need to add a comment to a line of code that increments a variable. But if you’re doing something weird that may not seem obvious to most developers, then you could add a comment to explain why.

There are different types of comments. Documentation comments explain the purpose and usage of code elements like functions and classes. They are often used with documentation tools to help other developers understand how to use a library or API. Here’s an example of a function with documentation comments in JSDoc syntax:

 

/**
* Calculates the total price of items in a shopping cart.
*
* @param {Array} items - An array of item objects, each with a `name` and `price`.
* @returns {number} The total price of all items in the cart.
*
* @example
* const cart = [
*     { name: 'Apple', price: 1.00 },
*     { name: 'Banana', price: 0.50 }
* ];
* const total = calculateTotalPrice(cart);
* console.log(total); // Outputs: 1.50
*/
function calculateTotalPrice(items) {
    let total = 0;

    items.forEach(item => {
        total += item.price; // Add each item's price to the total
    });

    return total; // Return the final total price
}

Clarification comments explain specific parts of the code that may not be immediately clear. While they can provide helpful context, relying too heavily on these comments can indicate that the code itself is not self-explanatory. Here’s an example:

 

// Initialize the sum variable, used to accumulate the total of the numbers
let num = 0; // sum will store the total sum of numbers from the array

// Initialize the array of numbers, the list from which we want to calculate the sum
let values = [5, 10, 15, 20, 25]; // The numbers array contains a set of values to sum

// Loop through each element in the numbers array, one by one
for (let i = 0; i < values.length; i++) {
    // 'i' is the index, and numbers[i] is the current element at that index
    let currentNumber = values[i]; // Get the current number in the iteration

    // Check if the current number is a valid number (a check for NaN)
    if (isNaN(currentNumber)) {
        // If the current number is NaN (Not a Number), we will skip this value
        continue; // Skip the current iteration and go to the next one
    }

    // If we get here, we know the currentNumber is valid
    num += currentNumber; // Add the valid number to the sum
}

// After the loop ends, we print the final sum
console.log("The total sum of the numbers is: " + num);
// We output the sum to the console for the user to see

In addition, avoid redundant, misleading, and funny (or sarcastic) comments.

Remember The DRY Principle (Don’t Repeat Yourself)

The DRY principle emphasizes the importance of reducing code duplication in our applications. The fundamental idea behind DRY is to have a single, authoritative representation of knowledge within our system. 

In simpler terms, if you find yourself writing the same code in multiple places, you’re not adhering to the DRY principle; this is your sign to find a better way to do it. You should aim to write code that can be reused instead of repeated.

Duplicated code can be a nightmare to maintain and modify, as you’ll often need to update the code in multiple places. Consider a scenario where we want to calculate the squares of a list of numbers:

 

const numbers = [1, 2, 3, 4, 5];

const squares1 = [];
for (let i = 0; i < numbers.length; i++) {
    squares1.push(numbers[i] * numbers[i]); // Calculate square for each number
}

const squares2 = [];
for (let j = 0; j < numbers.length; j++) {
    squares2.push(numbers[j] * numbers[j]); // Repeated logic for square calculation
}

console.log(squares1); // Outputs: [1, 4, 9, 16, 25]
console.log(squares2); // Outputs: [1, 4, 9, 16, 25]

This code violates the DRY principle in several ways:

  1. Code duplication: The logic to calculate the square of each number is repeated in two separate “for” loops, which is unnecessary.

  2. Maintenance challenges: If you need to change how the squares are calculated, you would have to update both for loops, which can be tedious if your codebase becomes fairly large.

  3. Cluttered code: The presence of duplicate loops makes the code harder to read and understand.

Now here's a refactored version that adheres to the DRY principle:

 

const numbers = [1, 2, 3, 4, 5];

function calculateSquares(arr) {
    const squares = [];
    for (let i = 0; i < arr.length; i++) {
        squares.push(arr[i] * arr[i]); // Calculate square for each number
    }
    return squares;
}

const squares = calculateSquares(numbers);
console.log(squares); // Outputs: [1, 4, 9, 16, 25]

Several improvements to note:

  1. Centralized logic: The logic for calculating squares is now encapsulated in a single function, calculateSquares, which takes an array as an argument. This eliminates redundancy.

  2. Easier maintenance: If you need to change how squares are calculated, you only need to update the logic in one place, making maintenance much more seamless.

  3. Reusability: The calculateSquares function can be reused with any array of numbers, making the code more flexible.

Most people would agree that this second code is much easier to work with than the first one.  

Non-Coding Tips For Keeping Your Code Clean And Secure

We’ve discussed general coding practices that, if adhered to, will make your codebase cleaner, more readable, and easier to maintain. However, several non-coding practices are just as essential for keeping code clean and secure. Let’s shift our focus to that.

Leverage SAST And DAST tools

Mistakes are inevitable, especially when humans are involved. That’s why it’s essential to integrate a robust testing system into your workflow to catch errors that developers may overlook. Incorporating static and dynamic security testing in your development workflow is crucial to keeping your code clean and secure. 

SAST tools analyze the source code without executing it, allowing developers to spot coding errors, insecure practices, and issues such as SQL injection, buffer overflows, and other vulnerabilities before they become problems in production.

 

DAST tools complement SAST by identifying runtime vulnerabilities in a live environment, allowing developers to detect issues that may not be apparent in static analysis. They simulate real-world attacks to assess how the application behaves under certain conditions, revealing hidden flaws that can cause issues.

Deploy Other Security Tools

In addition to SAST and DAST, it’s crucial to implement other security practices, such as SCA, penetration testing, Infrastructure as Code (IaC) security, secrets detection, and Cloud Security Posture Management (CSPM)—all offered by Codacy.

SCA helps ensure the security of third-party components, penetration testing simulates cyberattacks to uncover vulnerabilities, and IaC security automates the validation of infrastructure configurations.

CSPM ensures cloud environments don’t have misconfigurations, and secrets detection safeguards sensitive information within the code. Combining these security measures creates a robust defense strategy that protects the integrity and security of your applications.

Learn About Secure Coding Standards And Security Vulnerabilities

It helps to be up-to-date with secure coding standards like the OWASP Top 10, CWE/SANS Top 25, and any industry-specific guidelines relevant to your work. The more knowledge you have of these standards, the better you’ll get at writing cleaner code.

In addition, having a broad knowledge of common security vulnerabilities such as SQL injection, cross-site scripting (XSS), and authentication flaws can help you mitigate the risks of it occurring in your codebase.

After all, clean code lays a foundation for software security and quality.

Keep You Code Clean,  But Don't Overdo It

Writing clean code is important. However, “overcleaning” your code can have the opposite effect; it will make your code harder to read and understand. This will hurt productivity, as developers will have to constantly jump between many files/methods to make a simple change.

Follow clean code practices when writing code, but don’t overthink it in the early stages of your projects. Your priority should be to ensure that your code works and is well-tested. The cleaning part can come later when it’s time to refactor

If you're looking for a tool that automates keeping your code clean and secure as you write it, check out Codacy today.

RELATED
BLOG POSTS

How to Refactor Your Legacy Codebase
The phrase “legacy codebase” conjures up images of decades-old COBOL code that only one old-timer understands.
How To Write Clean Code
When you write code, you shouldn't just focus on making it work. You should also aim to make it readable, understandable, and maintainable for future...
How To Keep Your AI-Generated Code Secure
Many software developers use AI coding assistants like ChatGPT and GitHub Copilot to significantly speed up their development process, relying on them...

Automate code
reviews on your commits and pull request

Group 13