Laptop with code

Writing Clean, Maintainable Code: Tips and Best Practices

By Corey | coreyduffy | 23 Feb 2023


Writing clean, maintainable code can be a difficult process, especially when up against time-constraints. However, taking the time to make sure you write clean, quality code that is well tested will save you headaches in the long run and will increase your confidence in your code solutions (No waking up in a cold-sweat the night before a production release).

Photo by Kari Shea on Unsplash

There are a huge range of factors to consider when trying to write quality code. However, I’ve summarised a few of the main tips, tricks and guidelines I’ve picked up across my years as a Software Developer that I feel offer the best return on investment if implemented.

These tips don’t delve into processes like continuous integration and automation, or the planning considerations around allocating time regularly to keep code clean, functional and performant. Instead, I’ve focused more on the aspect of the day-to-day writing of code.

Make sure your code is well tested

I know it’s a boring way to start, but testing is one of the most discussed, debated and yet still underutilised processes by which you can make sure that your code is working as expected. Having a high level of code coverage and ensuring that your codebase is covered by quality unit, integration and system tests will give you the confidence needed to refactor and improve code, without questioning whether you are breaking any existing functionality by doing so.

For me, following Test-Driven Development (TDD) is the best way to make sure that your code is of a high standard of quality. It forces you to plan out your solution from a high-level and thus helps you make better design choices, and has the added bonus of making sure that you have a reliable suite of tests that will help keep your code clean and functioning properly into the future.

 

Clear variable and method names

Code should be self-descriptive. E.g. totalCostOfIceCream is always going to add more context than result.

 

Comments can be useful, but only if they aren’t too noisy.

There will be use-cases where comments in your codebase are valid and useful, but generally this is when they are used to explain business logic. Where possible, code should be easily understood without the need for comments through clear, useful variable and method names and through following some of the other tips below.

As an example, one use case where I find comments to be useful are when they can provide extra context to explain business logic in your codebase or explain why a non-intuitive solution has been implemented. e.g.

// Don't use obviousSolution() here as it isn't compatible with [unremovable dependency] 
public void someCode() {
...
}

 

Follow the DRY (Don’t Repeat Yourself) principle

Having the exact same or similar code in multiple locations in your codebase will only make it more difficult to maintain and update should requirements change.

Make sure that code isn’t copied and pasted throughout the codebase. Less duplication of code will leads to less headaches when debugging and changing specific functionality.

 

Follow the Single Responsibility Principle: limit functions/methods to one purpose and avoid any hidden effects

If I were to have a method named saveToDatabase(someObject), then the expectation would be that this method performs a single function, saving the supplied object to a database. The method shouldn’t perform any alterations to the object itself before it saves it to the database for example.

These hidden effects are particularly prevalent in legacy code, where developers have come along and added new functionality to existing methods that “works for what we need”, however, they often haven’t thought about the impacts their changes have had on the maintainability and readability of the codebase. Make sure your methods, classes, etc. aren’t trying to do too much.

 

Limit cognitive complexity in your methods.

Any conditional logic such as if statements or loops will add a level of complexity to your method, making it that bit harder to read and understand. If your methods contain a lot of nested if statements or loops, see if some of these can be extracted to their own methods with descriptive names. That way, the level of cognitive complexity per method is reduced and a reader of your code can grasp more easily what the code is expected to do.

E.g. the method below is hard to follow with the number of cognitively complex functions it contains:

public void hardToReadMethod() {
if (condition1 && condition2) {
if (condition3) {
for (Object item : items) {
if (condition4 && condition5) {
// do something
} else if (condition6) {
// do something else
} else {
// do something else
}
}
} else if (condition7 || condition8) {
for (Object otherItem : otherItems) {
for (Object nestedLoopItem : nestedLoopItems) {
if (condition9) {
// do something with both items
} else {
// do something with otherItem
}
}
}
} else {
// do another thing
}
} else {
// do some other thing
}
}

Extracting some of these cognitively complex functions and logic into their own methods can help make each individual method easier to read and debug, e.g.

public void easierToRead() {
if (shouldDoThing()) {
doSomething();
} else {
doOtherThing();
}
}

 

Try to avoid functions with many parameters.

A function with many parameters is often difficult to read and test.
One method of reducing the number of parameters passed to a function is to create a new object that encapsulates some/all the parameters that are being passed.

For example, instead of:
saveAddress(houseNumber, streetName, city, county, postCode)

A new object could be created:

public class Address { 
private String houseNumber;
private String streetName;
private String city;
private String county;
private String postCode;
}

and we could use this new object instead: saveAddress(address)

 

Follow the conventions of your codebase or language.

For example, if your codebase uses camel case for variable names then it would make sense for any variables you write to follow the same structure. This also applies to any naming conventions or formatting rules. e.g. if the rest of your codebase uses variables like totalCostOfIceCream, then it likely wouldn’t make sense for you to use total_cost_of_chocolate_cake as a name for a similar variable. These minor differences in conventions can make it more difficult when searching for a particular variable, method or class name in your code base.

 

Remove redundant code.

If code is no longer needed, then remove it. Less code in your codebase is less code for you to maintain going forward.

Following test-driven development principles and ensuring a high level of code coverage from your tests will ensure that you feel confident that removing code won’t cause any issues.

 

Don’t re-invent the wheel.

If a pre-existing library exists that provides the functionality you need for your application or solution, don’t be afraid to use it, if it’s safe to do so. Pre-existing libraries can help reduce your development time, keep your code clean by avoiding bloat and should already be well-tested.

 

Don’t bloat your project with dependencies.

In a bit of a contrast to the point above, make sure you aren’t importing more dependencies or libraries than you really need. More dependencies in your project mean more things that you may have to update and maintain.

 

Evolution, not revolution.

When implementing a new feature, or refactoring older code, it’s always better to make smaller changes more frequently. Trying to change too much at once will make your code much more difficult for others to review and can lead to bugs slipping through the cracks.


 

Thanks very much for reading this!

Hopefully these points have provided a starting point for how to make your code a bit cleaner and easier to maintain and have been helpful. It’s a common expression that “Software is never finished” and so it stands to reason that the process to keep that software to a high standard is also a never-ending one, but it’s a vitally important process nonetheless.

If you enjoyed this, please consider reading some of my other articles here or on Medium.

How do you rate this article?

2



coreyduffy
coreyduffy

My blog focuses on Java programming best practices, clean coding techniques, and my personal experiences as a developer. Join me on the journey of writing better code.

Send a $0.01 microtip in crypto to the author, and earn yourself as you read!

20% to author / 80% to me.
We pay the tips from our rewards pool.