Top 7 clean code tips every developer should know – Part #2

Top 7 clean code tips every developer should know – Part #2

Writing clean and maintainable code is a continuous journey for every developer, especially when working with .NET. Clean code ensures better collaboration, reduces technical debt, and leads to more efficient software development. In this second part, we’ll explore additional principles and techniques to further refine your coding skills.

Even experienced developers sometimes fall into bad habits that make code harder to read and maintain. If you’ve ever struggled to understand someone else’s code, or even your own after some time, this post is for you.

Let’s dive into more practical and effective tips that you can easily apply to your daily development workflow!

 

1 – Clear Methods, Clear Code!

Avoid generic names like Validate() or Check(). 

C#
				if(!Validate(selectedFile.Size))
			

Instead, opt for more descriptive names such as IsValidFileSize().

C#
				if(!IsValidFileSize(selectedFile.Size))
			

2 – Avoid Return Null for Collections

Returning null for collections might seem harmless, but it can lead to unnecessary null checks and potential runtime errors.

C#
				public IEnumerable<Product> GetAvailableProducts()
{
    if (noProductsFound)
    {
        return null; // Returning null when no products are found
    }

    return products;
}
			

Instead, return an empty collection to keep your code clean and resilient.

C#
				public IEnumerable<Product> GetAvailableProducts()
{
    if (noProductsFound)
    {
        return Enumerable.Empty<Product>();
    }

    return products;
}
			

Or, if you are using C# 12 +

C#
				public IEnumerable<Product> GetAvailableProducts()
{
    if (noProductsFound)
    {
        return [];
    }

    return products;
}
			

3 – Keep It Simple, Stupid (KISS)

The KISS principle reminds us that simple code is easier to read, debug, and extend. Overengineering or unnecessary abstractions can make your code harder to understand, even for experienced developers.

  • Avoid unnecessary abstractions
  • keep your architecture as simple as possible.
  • Reduce deeply nested conditions.
  • Choose straightforward solutions instead of overcomplicating logic.
  • Always prioritize readability over clever tricks.

 

Let’s look at an example where complexity can be reduced:

C#
				public bool CheckIfUserIsValidAndActiveAndHasPermissions(User user)
{
    if (user != null)
    {
        if (user.IsActive)
        {
            if (user.HasPermission)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        else
        {
            return false;
        }
    }
    else
    {
        return false;
    }
}
			
  • Overly long method name
  • Unnecessary nested if statements
  • Verbose and hard-to-read logic

 

Instead, we can simplify it:

C#
				public bool IsUserAllowed(User user) =>
        user is not null && user.IsActive && user.HasPermission;
			

Woooooow! Gosh! It’s turn a lot of more simple…

Of course, KISS is much more than just this! This is just a basic example to illustrate the concept. You should definitely dive deeper into this principle and explore how it can simplify complex code structures.

4 – Don’t Repeat Yourself (DRY)

The DRY principle (“Don’t Repeat Yourself”) is one of the foundations of clean code. He states that each item must have a unique representation in the code.

This helps avoid duplication of logic, making code easier to maintain, understand, and rework.

 

See an example below, without using DRY:

C#
				public void MakeTea(bool withSugar)
{
    if (withSugar)
    {
        Console.Write("Add sugar. ");
    }

    // Rest of the logic
}

public void MakeCoffee(bool withSugar)
{
    if (withSugar)
    {
        Console.Write("Add sugar. ");
    }

    //Rest of the logic
}
			

In this example, we can see the repetition of the sugar addition logic, in the near future if we need to add more items to drink, the code will become even more repeated and dirty, If I need to do maintenance, I will need to change all the methods to change the sugar logic.

 

A best practice would be:

C#
				public void BoilWaterAndAddSugar(bool withSugar)
{
    //logic

    if (withSugar) {
        Console.Write("Add sugar. ");
    }
}


			

Now that we have created a method to do the sugar logic, we can implement it in all the necessary methods, without repetition.

C#
				public void MakeTea(bool withSugar)
{
    BoilWaterAndAddSugar(withSugar);

    // Rest of the logic
}

public void MakeCoffee(bool withSugar)
{
    BoilWaterAndAddSugar(withSugar);

    //Rest of the logic
}
			

You can apply the DRY in different areas of software development, such as classes, methods, services and even the UI. By avoiding code duplication, DRY facilitates maintenance, improves readability and reduces the risk of errors. In the UI, for example, you can reuse components and layouts, while in classes and methods, you can extract common functionalities to promote reuse.

5 – Break Large Functions and Classes Into Smaller Ones with Single Responsibility

When working with large functions, the code tends to become harder to read and maintain. By breaking these functions into smaller ones that each handle a single responsibility, you not only improve readability but also make the code easier to test, debug, and extend.

Instead of creating a large function/class with multiple responsibilities:

C#
				public class Report
{
    public string Generate(Params...);
    public void SaveToFile(Params...);
    public void SendByEmail(Params...);
}
			

With Single Responsibility principle, we can divide it into smaller methods and classes:

C#
				public interface IReportGenerator
{
    string Generate(string data);
}

public interface IFileManager
{
    string Save(string content, string path);
}

public interface IEmailService
{
    bool Send(string recipient, string subject, string body);
}
			

By ensuring that a class/function has only one responsibility, it becomes easier to maintain and understand. It also helps in reducing the risk of unintended consequences when changes are made.

6 – A Function Should Only Do What Its Name Suggests

In programming, clarity and predictability are key to maintainable code. One way to achieve this is by ensuring that a function’s behavior aligns with its name. A function should only perform the actions its name suggests, expanding on the Single Responsibility Principle

When a Function Does Too Much:

C#
				public bool ValidateUserName(int id, string name)
{
    // Validation (Correct)
    if (string.IsNullOrWhiteSpace(name))
    {
        // logic
    }

    // Updating in the database (Should not be here)
    Console.WriteLine("User data saved to database.");
}

			

The ValidateUserName function should only validate user name, but it also saves it.

We can generate an error if someone calls the function thinking that within it, there would only be validations (without knowing that there is an update).

C#
				public bool ValidateUserName(string name)
{
    // Validation (Correct)
    if (string.IsNullOrWhiteSpace(name))
    {
        // logic
    }
}


public void UpdateUserName(int id, string name)
{
    Console.WriteLine("User data saved to database.");
}

			

Then we can call a function that calls these two methods and with a more explanatory name, avoiding this mistake makes your code much cleaner and more predictable!

7 – Don’t Use Different Terms for the Same Concept

Inconsistent naming can make your code harder to read and maintain. If you’re retrieving data, should you use Get, List or Fetch? If you’re creating something, should it be Create, Add or Insert?

Mixing these terms leads to confusion.

C#
				public List<Product> GetProducts() { /* ... */ }  
public List<User> ListUsers() { /* ... */ }  
public List<Content> FetchContent() { /* ... */ }  

			

Each method does a GET operation but uses different terms

C#
				public List<Product> GetProducts() { /* ... */ }  
public List<User> GetUsers() { /* ... */ }  
public List<Content> GetContent() { /* ... */ }  
			

Now it’s clear that all methods retrieve data.

POST Methods: Be Consistent Here Too

C#
				public void CreateUser(User user) { /* ... */ }  
public void AddProduct(Product product) { /* ... */ }  
public void InsertOrder(Order order) { /* ... */ }  

			

By keeping Get for retrieval and Add for insertion, your code becomes more predictable, readable, and maintainable.

C#
				public void AddUser(User user) { /* ... */ }  
public void AddProduct(Product product) { /* ... */ }  
public void AddOrder(Order order) { /* ... */ }  

			

 

Conclusion

I hope you enjoyed the tips shared in part two and that they’ve provided you with fresh perspectives to improve your coding practices. Remember, consistency and simplicity are key to writing clean, maintainable code that will stand the test of time.

Do you want me to dive into part 3? Let me know!

Let’s continue making our code even better, one step at a time

Thank you so much for reading until the end!

See you next time!

Share the Post:
plugins premium WordPress