Release your hidden classes

-

In a lot of software that I’ve seen, the class model could be better. One of the problems I often see is that a class contains other “hidden” classes: a set of fields that really should be its own class.

I will discuss where this originates from, and why it may cause problems.

First, I will provide two common examples. The first is the address, a set of at least street, house number, and city, but often also containing ZIP code, state and country. In a lot of cases these will just be fields in another class such as customer.
The second example is a date range; a period of time between a start date and an end date, for instance to indicate when a given rule is valid. Often, a date range is a start and end date in the class that it applies to.

What is the problem anyway?

So, why is it a problem if these are not represented as separate classes?
First, it may lead to duplication of code. If you have several date ranges in your code, there will be several places where you are going to check if a given date is in that date range, or if another date range overlaps. Similarly, there may be validation and rendering methods for an address. All this code must also be tested, sou you get a lot of duplication in tests as well.

Second, it makes the code less readable in many ways. If-statements comparing four dates from two date ranges is hard to understand, and tricky to debug. Quick, what does this do?

Java

LocalDate dateRangeStart = ...;
LocalDate dateRangeEnd = ...;
LocalDate otherDateRangeStart = ...;
LocalDate otherDateRangeStart = ...;

if (dateRangeEnd.isAfter(otherDateRangeStart) && dateRangeStart.isBefore(otherDateRangeEnd)) {
  ...
}

If a class contains multiple addresses, your field names will become less readable:

Java

class Customer {
  private String firstName;
  private String surname;
  private String residenceAddressStreet;
  private String residenceAddressHouseNumber;
  private String residenceAddressCity;
  private String postalAddressStreet;
  private String postalAddressHouseNumber;
  private String postalAddressCity;
  ...
}
1
2
3
4
5
6
7
8
9
10
11
class Customer {
  private String firstName;
  private String surname;
  private String residenceAddressStreet;
  private String residenceAddressHouseNumber;
  private String residenceAddressCity;
  private String postalAddressStreet;
  private String postalAddressHouseNumber;
  private String postalAddressCity;
  ...
}

This may be the right moment to consider why classes are modelled this way. There may be a lot of different reasons, but one of the most important ones I see is that an application blindly copies an external data model. If a database is provided, and the database has a table “customer” with street, house number and city fields, there will be a class Customer with those same fields. Similarly, if the interface to a front end specifies the address as fields in a larger object instead of a separate object, that model may easily become the back end model as well.
Another reason may be that no suitable class is available. Java has great support for dates and durations, but not for a date range. So the easy way is to just add a start and end date to your class.

Type safety

Now, for a last warning: upon discovering the code duplication and readability problems, one might be tempted to create static helper methods in utility classes, because this has less impact on the code as a whole. Apart from all the reasons why utility classes should be avoided, I want to add one more specific to this case: since many fields of an address are Strings, any helper method will have a signature that does not check whether you pass the right fields:

class AddressHelper() {
  public static boolean isAddressValid(String street, String houseNumber, String city) {
    ...
  }
}

// No compile error
if (AddressHelper.isAddressValid(houseNumber, street, city) {
  ...
}

Especially error-prone because different countries have a different order in which they present the address fields: compare the Dutch street – house number – ZIP code – city to the US house number – street – city – ZIP code.

Using separate classes

If you represent an address as a separate class, it is far easier to provide it to an external service in a type-safe manner. It also makes your code more readable.

class Address {
  private String street;
  private String houseNumber;
  private String city;
}

class Customer {
  private String firstName;
  private String surname;
  private Address residenceAddress;
  private Address postalAddress;
}

class AddressService() {
  public boolean isAddressValid(Address address) {
    ...
  }
}

if (addressService.isAddressValid(customer.getResidenceAddress())) {
  ...
}

class DateRange {
  private LocalDate start;
  private LocalDate end;

  ...

  public boolean overlaps(DateRange other) {
    return end.isAfter(other.getStart()) && start.isBefore(other.getEnd());
  }
}

if (dateRange.overlaps(otherDateRange)) {
  ...
}

In closing

Refactoring may incur significant cost to the development project, and the later you start the more it will cost. However, operational cost is significant over the lifetime of a software product, and solid, readable code will drive that cost down. Experience will make it easier to spot these problems earlier, but even if you run into the problems late in a project, it is worth considering refactoring your class model.