Lecture 4: Getting (back) into Java, part 2
Objectives of the lecture
1 A compact representation for durations
1.1 Interface polymorphism
1.2 Implementing the new representation
2 An abstract base class
2.1 The factory method pattern
2.2 How far should we go?
8.9

Lecture 4: Getting (back) into Java, part 2

Objectives of the lecture

In this lecture we will add an alternative implementation of the design from the last lecture. This will simulate changing an implementation after it has been written, tested and possibly used. We will also see how to minimize code duplications, and in doing so, identify design ideas that were neither obvious nor motivated initially.

1 A compact representation for durations

In the previous lecture we implemented working Duration objects, and we could start writing code with them. (You likely will.) But along the way we may have noticed that working with the hours-minutes-days representation is somewhat awkward. Perhaps when we chose that over a single field representing the total number of seconds, we made a wrong turn.

We wish to implement the Duration interface again. We also wish to minimize any changes to the code that uses our earlier implementation (i.e. client code). This situation occurs quite often: how do we design so that components can be easily replaced by better versions of themselves?

We observe how we can achieve this by taking advantage of some of our design choices:

1.1 Interface polymorphism

In the remainder of this lecture we pursue the latter strategy, since all the code required for the former is required for the latter as well. We name the new class CompactDuration; perhaps we should have given DurationImpl a more descriptive name. Once CompactDuration is written, our class hierarchy will look like this:

Duration interface with two implementations

This is a UML class diagram with the following standard notations:

This diagram lists all the fields and methods of a class and relationships between them. Often it is abbreviated to show only some of these aspects to emphasize on some aspect of them.

How does this design help us? Consider the situations of two clients that were written to use durations back when DurationImpl was the only implementation, once CompactDuration implements Duration as well:

Duration interface with clients and two implementations

UnhappyClient uses the DurationImpl class in its code, perhaps as the type of some fields or method parameters. This means that to use CompactDuration instead, the maintainer of UnhappyClient needs to change its code everywhere that it mentions DurationImpl to use CompactDuration. This is an example of strong coupling between two classes: a design artifact that we aim to minimize.

HappyClient uses the Duration interface in its code as the type for fields and parameters that need to hold durations. Because a DurationImpl is a Duration, the Durations that we give HappyClient can be DurationImpls and will work properly. And because the new class, CompactDuration is a Duration as well, durations that we use with HappyClient can be CompactDurations as well, with no changes to HappyClient. In other words HappyClient refers to the interface Duration rather than a particular class implementing it. We accrue the benefits of interface polymorphism–the ability to use multiple classes that implement an interface with a client that depends only on the interface.

In reality, often a happy client will not able to decouple itself completely from a specific implementation. This is because objects of the implementation would need to be created somewhere, mandating the mention of the class name in constructors. In this case, we would need to make a limited number of changes to the nearly happy client to update the constructors. We can isolate these changes to one or a few places so that these changes, although unavoidable, are manageable. We will see later a way to design a client that avoids even calling a particular class’s constructor, making the client completely decoupled from the class.

1.2 Implementing the new representation

The new class should be straightforward. We start by writing one of the two constructors (the simpler one) and the single private long field that it initializes:

/**
 * Durations represented compactly, with a range of 0 to
 * 2<sup>63</sup>-1 seconds.
 */
public final class CompactDuration implements Duration {
  /**
   * Constructs a duration in terms of its length in seconds.
   *
   * @param inSeconds the number of seconds (non-negative)
   * @throws IllegalArgumentException {@code inSeconds} is negative
   */
  public CompactDuration(long inSeconds) {
    if (inSeconds < 0) {
      throw new IllegalArgumentException("must be non-negative");
    }

    this.inSeconds = inSeconds;
  }

  private final long inSeconds;
}

When we create the other constructor, we might notice something funny:

/**
 * Constructs a duration in terms of its length in hours, minutes, and
 * seconds.
 *
 * @param hours the number of hours
 * @param minutes the number of minutes
 * @param seconds the number of inSeconds
 * @throws IllegalArgumentException if any argument is negative
 */
public CompactDuration(int hours, int minutes, int seconds) {
  if (hours < 0 || minutes < 0 || seconds < 0) {
    throw new IllegalArgumentException("must be non-negative");
  }

  inSeconds = 3600 * hours + 60 * minutes + seconds;
}

We’ve seen this code before (albeit not all in one place)! Repetition should bother us, but will it bother us enough for us to do something about it? The first two methods probably won’t:

@Override
public long inSeconds() {
  return inSeconds;
}

@Override
public Duration plus(Duration that) {
  return new CompactDuration(this.inSeconds + that.inSeconds());
}

However, when we get to asHms, we see code repeated from both DurationImpl’s unary constructor and its own asHms method:

@Override
public String asHms() {
  int hours = (int) (inSeconds / 3600);  // that overflow again!
  int minutes = (int) (inSeconds / 60 % 60);
  int seconds = (int) (inSeconds % 60);

  return String.format("%d:%02d:%02d", hours, minutes, seconds);
}

Looking forward, we anticipate more repetition in other methods. Larger, more complex classes are likely to have even more redundancy. It would be desirable to somehow factor out the common functionality of the two classes.

2 An abstract base class

When we want to factor out redundancy between functions, we create a helper function and call it from the other functions. When we want to factor out redundancy between classes, we create a helper class and refer to it from the other classes. One way to use the helper class from the classes that share it is to have them extend it. Extension, also known as implementation inheritance, derives a new class from an older one by adding fields, adding methods, and replacing methods. Furthermore, we can design an abstract class with extension in mind, leaving some of its methods missing for subclasses to define.

We begin by creating new, empty abstract class AbstractDuration and insert it between the Duration interface and its implementations:

Duration hierarchy with empty AbstractDuration

We do this by defining the new class so that it implements Duration and modifying the two concrete classes to extend the new AbstractDuration class:

abstract class AbstractDuration implements Duration {
  // nothing yet
}

public final class DurationImpl extends AbstractDuration { ... }

public final class CompactDuration extends AbstractDuration { ... }

Note that AbstractDuration has package scope rather than public, because it’s an implementation detail shared by our two classes but not part of the public API for durations. (There’s no reason we couldn’t make it so.)

Adding AbstractDuration by itself accomplishes nothing, but now we’re set up to start moving common functionality from DurationImpl and CompactDuration into AbstractDuration, allowing them to share. Some of these changes are easy, as four of the methods in DurationImpl will work if moved to AbstractDuration with no changes:

Refactoring the Duration hierarchy

These methods move easily because each of them is written in terms of method inSeconds() from the Duration interface. For AbstractDuration itself, inSeconds() remains undefined, but each concrete subclass must define it, which means that AbstractDuration can rely on each concrete subclass’s inSeconds() method.

2.1 The factory method pattern

Moving plus(Duration) into AbstractDuration is harder. To see why, consider its definition in DurationImpl:

@Override
public Duration plus(Duration that) {
  return new DurationImpl(this.inSeconds() + that.inSeconds());
}

This definition of plus will still work if we move it to AbstractDuration, but with an unfortunate twist: because the DurationImpl constructor is hard-coded into the method, all implementations of Duration would return an object of one specific implementation, DurationImpl. It would be better to allow each class to choose whatever class it likes for the result of plus.

How can we do it better? Instead of hard-coding the constructor call into plus (in AbstractDuration), suppose we had a method that would construct the right class of Duration. Then we could rewrite plus to use that:

@Override
public Duration plus(Duration that) {
  return fromSeconds(this.inSeconds() + that.inSeconds());
}

Now we just need to implement fromSeconds so that it constructs durations of the right class, as determined by each concrete subclass of AbstractDuration. All subclasses of AbstractDuration would implement this method and return an object of a suitable type (likely itself). Thus we have written a method that has the ability to return objects of several different types, depending on which object was used to call it. This is called a factory method.

/**
 * Constructs a {@link Duration} in a manner selected by each concrete
 * subclass of this class.
 *
 * @param asSeconds the length in seconds
 * @return the new {@code Duration}
 */
protected abstract Duration fromSeconds(long inSeconds);

We make the method protected because it exists only for communication between AbstractDuration and its subclasses, and does not need to be visible to clients.

Concrete subclasses are required to define any abstract methods of their superclasses, which means that now we just need to define fromSeconds in each of the subclasses of AbstractDuration to do the right thing. That is, in DurationImpl we write

protected Duration fromSeconds(long inSeconds) {
  return new DurationImpl(inSeconds);
}

And in CompactDuration we write

protected Duration fromSeconds(long inSeconds) {
  return new CompactDuration(inSeconds);
}

Now we can move plus to AbstractDuration and it will use its subclasses’ definitions of fromSeconds to construct its result:

Using the factory method pattern to defer construction to subclasses

Note that # indicates that a method is protected.

A factory method is often used in the context of creating objects, and hence is an example of a creational design pattern. A factory method may decide which object to instantiate in several ways. One way is how it is used here: each class implements the method to return an object of itself. AbstractDuration can defer the decision of what class to instantiate to its subclasses by declaring the factory method abstract:1There are two potential points of confusion here: 1) Not every factory method is an instance of the factory method pattern, but rather those that work in the particular way introduced here. 2) We are declaring our factory method to be abstract, but the abstract factory pattern, which we will see later, means something else more specific.A second way is for a factory method to accept one or more arguments that specify which type of object must be created.

2.2 How far should we go?

While at this point we have moved all of the methods to AbstractDuration that we can, that doesn’t mean we’ve eliminated all the redundancy that we can. In particular, let’s return to the method of CompactDuration whose redundancies with parts of DurationImpl motivated creating the abstract class in the first place:

@Override
public String asHms() {
  int hours = (int) (inSeconds / 3600);  // that overflow again!
  int minutes = (int) (inSeconds / 60 % 60);
  int seconds = (int) (inSeconds % 60);

  return String.format("%d:%02d:%02d", hours, minutes, seconds);
}

Compare the above to this constructor and method of DurationImpl:

public DurationImpl(long inSeconds) {
  if (inSeconds < 0) {
    throw new IllegalArgumentException("must be non-negative");
  }

  this.seconds = (int) (inSeconds % 60);
  this.minutes = (int) (inSeconds / 60 % 60);
  this.hours = (int) (inSeconds / 3600);  // overflow :(
}

@Override
public String asHms() {
  return String.format("%d:%02d:%02d", hours, minutes, seconds);
}

CompactDuration.asHms() uses the same code as the DurationImpl(long) constructor to break the duration into hours, minutes, and seconds, and then it uses the same code as DurationImpl.asHms() to do the formatting. Thus, if we are willing to factor out some helper methods, we can make a single point of control for each of these aspects. In particular, we add four protected, static methods to AbstractDuration to define each piece of common logic. For the formatting logic, we define asHms(int, int, int):

protected static String asHms(int hours, int minutes, int seconds) {
  return String.format("%d:%02d:%02d", hours, minutes, seconds);
}

Note that this new asHms method is static, meaning that it doesn’t operate on a particular object, instead taking its input as explicit arguments.

The other three methods are for extracting the three components from the duration. We also take this opportunity to fix the overflow bug from earlier—it’s a good time to do it, since we can now write the overflow check in this one method rather than two different places.

protected static int minutesOf(long inSeconds) {
  return (int) (inSeconds / 60 % 60);
}

protected static int secondsOf(long inSeconds) {
  return (int) (inSeconds % 60);
}

protected static int hoursOf(long inSeconds) {
  if (inSeconds / 3600 > Integer.MAX_VALUE) {
    throw new ArithmeticException("result cannot fit in type");
  }

  return (int) (inSeconds / 3600);
}

Now we can rewrite the offending methods and constructor to use the common functionality provided by the abstract base class:

// in CompactDuration:
  @Override
  public String asHms() {
    return asHms(hoursOf(inSeconds),
                 minutesOf(inSeconds),
                 secondsOf(inSeconds));
  }

// in DurationImpl:
  @Override
  public String asHms() {
    return asHms(hours, minutes, seconds);
  }

  public DurationImpl(long inSeconds) {
    if (inSeconds < 0) {
      throw new IllegalArgumentException("must be non-negative");
    }

    this.seconds = secondsOf(inSeconds);
    this.minutes = minutesOf(inSeconds);
    this.hours = hoursOf(inSeconds);
  }

Not only did the refactoring allow us to eliminate redundancy but it made the code clearer as well. We could keep going, next by factoring out some error-checking code, but we are probably nearing diminishing returns. Here’s what we’ve ended up with:

The Duration hierachy, refactored

One last UML convention: underlining means static.

1There are two potential points of confusion here: 1) Not every factory method is an instance of the factory method pattern, but rather those that work in the particular way introduced here. 2) We are declaring our factory method to be abstract, but the abstract factory pattern, which we will see later, means something else more specific.