5 rules for writing readable code

Pragmatic Nerdz
Dec 31, 2020 - 5 Minutes

Have you ever reviewed a code that you've written weeks, months or years ago; and then you realize you do not understand what it's doing? that it's too complex? Then imagine what will be the experience of developers other than you when they will review it!

This is a sign of code having poor readability!

Why code readability is important?

There are 2 audiences for any code:

  1. The computer that is going to execute that code, once translated to 01010 by compilers.
  2. The developers who are going to maintain that code in the programming language it has been written.

The purpose of code readability is to help developers to easily read, understand and maintain that code.

This is what happens when working with code having poor readability:

  • You might misunderstand the code and use it in ways it was not meant to be used. 
  • You will then spend multiple iterations getting the change right. 
  • Or you might change the code, unintentionally breaking functionality elsewhere.

Overall, you waste more time with such code and it lead a a lot frustrations. Readable code is easy understand and change. This mean we can add new features or fix bugs much faster.

How to make readable code

Look at the following example of code, it extracts all the transactions associated with the event read from a CSV file and store them into another CSV file.

class DataExtractor {
  public int extract(File in, File out) {
     List<String> lines;
     try {
        lines = Files.readAllLines(in.toPath());
     } catch (IOException e){
        return 0
     }

     List<String> extracted = new ArrayList();
     for (int i=0 ; i<lines.length ; i++){
         String line = lines[i];
         String[] columns = line.split(",");
         if (columns.size == 4 && "1".equals(columns[1]){
            extracted.add(line)
         }
     }

     try {
       Files.write(out.toPath(), extracted);
     } catch(IOException e){
       e.printStackTrace(System.err);
     }
     return filtered.size();
  }
}

We are going to apply 5 rules to improve the readability of this code.

1. Don't use magic numbers

Magic number are values that are used directly in the code. Instead, you should declare then as constants that you'll use in the code.

public class DataExtractor {
  private static final int COLUMN_COUNT = 4;
  private static final String EVENT_READ = "1"

  public int extract(File in, File out) {
     ...
     List<String> extracted = new ArrayList();
     for (int i=0 ; i<lines.length ; i++){
         String line = lines[i];
         String[] columns = line.split(",");
         if (columns.size == COLUMN_COUNT && EVENT_READ.equals(columns[1]){
            extracted.add(line)
         }
     }
     ...
  }
}

2. Simplify your conditional statements

if statements having more that 1 conditions should be reduced. Move all the conditions to a private function having a name explaining the conditions evaluated.

public class DataExtractor {
  private static final int COLUMN_COUNT = 4;
  private static final String EVENT_READ = "1";

  public int extract(File in, File out) {
     ...
     List<String> extracted = new ArrayList();
     for (int i=0 ; i<lines.length ; i++){
         String line = lines[i];
         String[] columns = line.split(",");
         if (isReadTransaction(column)){
            extracted.add(line)
         }
     }
     ...
  }

  private boolean isReadTransaction(String[] columns) {
     return columns.size == COLUMN_COUNT && EVENT_READ.equals(columns[1]
  }
}
Receive my Stories your e-mail inbox as soon as I publish them.
Subscribe to my Blog

3. Don't handle exceptions

Unless you know what to do with the exception, do not handle them, and let the caller of your function deal with it.

In our exemple, extract() is extracting the read transactions but is also handling IOException. To simplify this function, we are going remove the responsibility of handling exceptions by changing its signature to:

public int extract(File in, File out) throws IOException

By adding to the declaration throws IOException, we are transferring the responsibility of exception handling to the caller. Now the code is much simpler:

public class DataExtractor {
  private static final int COLUMN_COUNT = 4;
  private static final String EVENT_READ = "1";

  public int extract(File in, File out) throws IOException {
     List<String> lines = Files.readAllLines(in.toPath());

     List<String> extracted = new ArrayList();
     for (int i=0 ; i<lines.length ; i++){
         String line = lines[i];
         String[] columns = line.split(",");
         if (isReadTransaction(column)){
            extracted.add(line)
         }
     }

     Files.write(out.toPath(), extracted);
     return filtered.size();
  }

  private boolean isReadTransaction(String[] columns) {
     return columns.size == COLUMN_COUNT && EVENT_READ.equals(columns[1]
  }
}

4. Write short functions

A short function is a function that is reduced the minimal number of instructions.

If you look at the extract(), at high level it has 3 blocks of code: read a file, extract read transactions and write to a file. To improve readability if extract(), we will move those 3 blocs of code into private functions

public class DataExtractor {
  private static final int COLUMN_COUNT = 4;
  private static final String EVENT_READ = "1";

  public int extract(File in, File out) throws IOException {
     List<String> lines = read(in);
     List<String> extracted = extract(lines);
     write(extracted, out)
     return filtered.size();
  }

  private List<String> read(File in) throws IOException {
     return files.readAllLines(in.toPath());
  }

  private List<String> extract(List<String> lines) {
     List<String> extracted = new ArrayList();
     for (int i=0 ; i<lines.length ; i++){
         String line = lines[i];
         String[] columns = line.split(",");
         if (isReadTransaction(column)){
            extracted.add(line)
         }
     }
  }

  private void write(List<String> extracted, File out) throws IOException {
    Files.write(out.toPath(), extracted);
  }

  private boolean isReadTransaction(String[] columns) {
     return columns.size == COLUMN_COUNT && EVENT_READ.equals(columns[1]
  }
}

Note that as we are moving code to functions, we make sure that each of the function do not handle exceptions!

5. Use meaningful names

Name you use for functions of variable are critical to improve the readability of the code. It helps you to understand what the code is doing, without going in the the details of each function.

Look at the changes we did in the code:

public class DataExtractor {
  private static final int COLUMN_COUNT = 4;
  private static final String EVENT_READ = "1";

  public int extract(File in, File out) throws IOException {
     List<String> transactions = readTransactions(in);
     List<String> readTransactions = extractReadTransactions(transactions);
     writeTransactions(extracted, out)
     return readTransactions.size();
  }

  private List<String> readTransactions(File in) throws IOException {
   ...
  }

  private List<String> extractReadTransactions(List<String> lines) {
   ...
  }

  private void writeTransactions(List<String> transactions, File out) throws IOException {
    ...
  }
  ...
}

Coding is a social activity. When writing your code, other than ensuring that it works, you must always make sure that other developers will be able to read and understand it. Improving code readability leads to simpler code, simpler code is easier to maintain.

Readability is one of the most important measure of code quality


Clean Code
Code
Code Smell
Best Practices
Readability