How to Write Clean Code

Krantharat Krongsiriwat
5 min readDec 4, 2020

Everybody loves clean code, but have you ever wondered what constitutes it? After reading Clean Code: A Handbook for Agile Software Craftsmanship by Robert C. Martin, I have a better picture of clean code.

Why Clean Code

Have you ever rushed to get tasks done, intending to clean up the code later, but those future days never come?

Over time we gradually build the mess. The mess becomes so large driving the team productivity slowdown. Management team step up and plan to have the application redesigned. Now we have two teams in a race, one team maintaining the old system, and the new one building a new system that does everything the old system does and keeping up with the changes. The race might take years and hinder the productivity. In the worst case, the new design becomes another pile of mess and cannot be of use to the company.

Desperate, isn’t it? No one ever wants this. Developers swear having to work on bad code. Management team hates to see the decrease in productivity.

It is therefore fatal to avoid having such a mess and keep the code clean — easy to understand and ready for change

How to Write Clean Code

There is no consensus what are needed for clean code. But, from my experience so far, I believe these points are a must for having clean code.

Meaningful Names

Use the name that expresses its intent, and replace magic numbers by constants with good naming. In the below example, it is hard to reason about what the function does.

public void foo(Account a1, Account a2, double x){
if (a1.b == a2.b) {
a1.x -= x;
a2.x += x;
} else {
a1.x -= x;
a1.x -= 50;
a2.x += x;
}
}

After renaming it appropriately, we know that it is the function to transfer amount between accounts.

public void transfer(Account senderAccount, Account receiverAccount, double amount){
if (senderAccount.branchId == receiverAccount.branchId) {
senderAccount.balance -= amount;
receiverAccount.balance += amount;
} else {
senderAccount.balance -= amount;
senderAccount.balance -= TRANSFERRING_FEE;
receiverAccount.balance += amount;
}
}

Formatting

Formatting is important. Readers can benefit from indentations, blank lines and spaces. Grouping the same concepts together and separating each of them with blank lines also helps.

One example is when writing a test. We can separate into 3 parts: given, when and then.

// Given
Account adamAccount = Account.create("Adam", 5000.00);
Account ernestAccount = Account.create("Ernest", 1500.00);
// When
underTest.transfer(adamAccount, ernestAccount, 1000.00)
// Then
assert(adamAccount.getBalance() == 4000.00);
assert(ernestAccount.getBalance() == 2500.00);

Consistency

We should have descriptive name and also consistent naming convention. For instance, get functions are normally implemented without side effects. Implementing a get function with side effects makes an application error-prone.

private long branchId;
public long getBranchId() { // without side effects
return branchId
}
private long uniqueId;
public long getUniqueId() { // with side effects
return uniqueId++;
}

Having Tests

Tests reflect the ability to change code. Dirty tests imply that we have less ability to change code with confidence that nothing will break, because it is hard to understand what the test tries to verify. It is therefore necessary to have readable and maintainable tests, allowing a team to complete tasks more confidently and quickly.

Single Responsibility

Single responsibility states that a class or module should have one and only one, reason to change

Single responsibility concept encourages clean code. It helps readers understand what it tries to achieve as it does only one thing. It is also easy to change because developers can find with ease where to work on to satisfy the requirement. This concept should be applied to not only class levels, but the following:

  • Testing — Each test cases should verify only one concept.
  • Function — Each function should do one thing.
  • Class — Each class should have one responsibility and has one reason to change.

Although the concept is straightforward, it is hard to identify when it is doing too much. One way to check is to describe in our own words what tests, functions and classes do. The word “and” signals that it is possible to have it refactored.

In our transfer method above, we can make a tiny change to improve readability and maintainability.

public void transfer(Account senderAccount, Account receiverAccount, double amount){
senderAccount.balance -= amount;
senderAccount.balance -= getTransferFee(senderAccount, receiverAccount);
receiverAccount.balance += amount;
}
private double getTransferFee(Account acc1, Account acc2) {
if (acc1.branchId == acc2.branchId) {
return SAME_BRANCH_TRANSFERRING_FEE ;
} else {
return DIFFERENT_BRANCH_TRANSFERRING_FEE;
}
}

Isolation from Change

This is similar to Dependency Inversion Principle, which says that our classes should depend upon abstractions, not on concrete details. Codes can change often according to business’s demands. If a client class depending on those concrete details, it is at risk that when the details changed.

In the following example, we define AccountRepository that have findById method. It is unnecessary to know how it is implemented. We need to believe that it can findById correctly. This way we can change how to find an account by id from the repository without affecting the code below.

interface AccountRepository {
Account findById(long accountId);
}
private AccountRepository accountRepository;public void closeAccount(long accountId) {
Account account = accountRepository.findById(accountId);
account.close();
}

Don’t Repeat Yourself

Duplication in code discourages maintainability because changes, if any, have to be applied many places instead of one, which to some extent bring bugs to applications. It is advisable to extract common logics into functions and reuse them.

Separate Constructing a System from Using It

The application should have no knowledge of the construction process. The lazy initialization of AccountRepository below should be avoided since it violates Single Responsibility Principle and complicates testing. We might leverage Dependency Injection to separate the process.

public class BankService {
private AccountRepository accountRepository;
private AccountRepository getAccountRepository() {
if (accountRepository == null) {
accountRepository = new AccountReposiotory();
}
return accountRepository;
}
...}

How to Achieve Clean Code

Now we have a sense of how clean code looks. It has not been paid much attention to though, as we care more about making it work quickly. Clean code, quite the opposite, requires time and attention.

Plan

When we plan to complete any tasks, the time spared tidying the code should be considered properly.

Refine

Clean code comes with effort. Like writing an email, you have a draft. You read through it, make sure there is no typo and it is understandable by the audience. Writing code has no difference. You have your first draft and you need to polish it until it reads well.

Improve

Leave the campground cleaner than you found it. — Boy Scout Rule

Most of the time you modify existing code instead of writing new one, and that existing code is nothing but clutter. What you can do is to always make it better. For example, Before you change the code, you spend several hours to understand the logic. By making it more readable, other developers (including yourself in the future) that need to read or update the code can learn more easily about the logic, saving them multiple hours.

Codebase is like a workplace for developers. Making it tidy and clean rescues developers from sluggish productivity. It also allows managers to focus on working on a brand new product instead of fixing the old one. But there is no free lunch. Clean code requires attention from both parties.

Always keep that in mind and happy clean coding!

--

--