5 tips for simplifying your classes

By Wutsi Tech Blog
4 Minutes

I've wrote an article showing how to improve code readability, and someone pointed at me rightfully that this approach may lead to bloated classes, having too many functions.

This article will show some techniques for simplifying your classes.

1. Reduce your classes responsibilities

To reduce your classes, the first this you should make sure is that they only have one responsibility. In other words:

A class should have only one reason to change

Robert C. Martin

Looking at the following code from the previous article.

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 {
    ...
  }
  private boolean isReadTransaction(String[] columns) {
   ...
  }
}

We can argue that DataExtractor too many responsibilities. It can be changed if:

  1. We want to alter how we extract the transactions
  2. We want to alter how to read transactions from a file
  3. We want to alter how to write transactions into a file.

The strategy to reduce the responsibilities of DataExtractor is to move the extra responsibilities to other classes.

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 = new TransactionReader().readTransactions(in);
     List<String> readTransactions = extractReadTransactions(transactions);
     new TransactionWriter().writeTransactions(extracted, out)
     return readTransactions.size();
  }


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

  private boolean isReadTransaction(String[] columns) {
   ...
  }
}


public class TransactionReader {
  public List<String> readTransactions(File in) throws IOException {
   ...
  }

}


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

}

With these changes, we now have 3 classes having a single responsibility:

  • Changing how we extract transactions will impact only DataExtractor
  • Changing how we read transactions  will impact only TransactionReader
  • Changing how we write transactions will impact only TransactionWriter

2. Reduce your functions responsibilities

The single responsibility principe should also be applied to your functions. Each function should do only one thing!

public class TransactionWriter {
  public void writeTransactions(List<String> transactions, File out) throws IOException {
    String extension = FilenameUtils.getExtension();
    if ("xml".equals(extension)){
      writeXml(transactions, out);
    } else if ("json".equals(extension)){
      writeJson(transactions, out);
    } else {
      writeText(transactions, out);
    }
  }

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

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

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

TransactionWriter.writeTransactions() owns several strategies for serializing transactions to file, we can move each strategy to one class.

public class TransactionWriter {
  public void writeTransactions(List<String> transactions, File out) throws IOException {
    String extension = FilenameUtils.getExtension();
    if ("xml".equals(extension)){
      new FileWriterXml().writeXml(transactions, out);
    } else if ("json".equals(extension)){
      new FileWriterJson().writeJson(transactions, out);
    } else {
      new FileWriterTxt().writeTxt(transactions, out);
    }
  }
}

public class FileWriterXml {
  public void writeXml(List<String> transactions, File out) throws IOException { 
    ...
  }
}

public class FileWriterJson {
  public void writeJson(List<String> transactions, File out) throws IOException { 
    ...
  }
}

public class FileWriterTxt {
  private void writeTxt(List<String> transactions, File out) throws IOException { 
    ...
  }
}

3. Extract interfaces

FileWriterXml, FileWriterJson and FileWriterTxt are just 3 differents strategies for writing transaction into files. So we can extract the concept of writing to an interface FileWriter.

public interface FileWriter {
   public void write(List<String> transactions, File out) throws IOException;
}

public class FileWriterXml extends FileWriter {
  public void write(List<String> transactions, File out) throws IOException { 
    ...
  }
}

public class FileWriterJson extends FileWriter {
  public void write(List<String> transactions, File out) throws IOException { 
    ...
  }
}

public class FileWriterTxt extends FileWriter {
  private void write(List<String> transactions, File out) throws IOException { 
    ...
  }
}

This help us to add an abstraction for representing different strategies we can use for writing transactions to files.

4. Switch to Factory

Now that writing to file has been abstracted, we will isolate writeTransactions() from knowing which strategy used for writing the transactions, by delegating the creation of FileWriter to FileWriterFactory 

public class TransactionWriter {
  private FileWriterFactory fileWriterFactory = new FileWriterFactory();

  public void writeTransactions(List<String> transactions, File out) throws IOException {
    FileWriter writer = fileWriterFactory.getFileWriter(file);
    writer.write(transaction, out);
  }
}

public class FileWriterFactory {
   public void getFileWriter(File file){
      String extension = FilenameUtils.getExtension();
      if ("xml".equals(extension)){
          return new FileWriterXml();
      } else if ("json".equals(extension)){
          return new FileWriterJson();
      } else {
          return new FileWriterTxt();
      }
   }
}

5. Simplify your domain

Another common issue is when we fail to add abstractions when creating our domain objects and database tables.

An example is the following User class.

public class User {
   private long id;
   private String name;
   private String email;
   private String street;
   private String country;
   private String city;
   private String mobilePhoneNumber;
   private String mobilePhoneCarrier;
   private String homePhoneNumber;
   private String homePhoneCarrier;
   private String officePhoneNumber;
   private String officePhoneExtension;
}

Another version of the same class, but with the Address and Phone abstractions, we end up with a more concise version of User.

public class User {
   private long id;
   private String name;
   private String email;
   private Address address;
   private Phone mobilePhone;
   private Phone homePhone;
   private Phone officePhone;
}

public class Address {
   private String street;
   private String country;
   private String city;
}

public class Phone {
   private String number;
   private String carrier;
   private String extension;
}

Best Practices
Clean Code
Code Smell