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:
Information hiding: Because we made the fields in
DurationImpl
private, client code cannot depend on that representational choice (since it cannot access these fields). So if we’re sure that the compact representation is strictly an improvement over the HMS representation, and if we want all our code, old and new, to use that instead, we can modifyDurationImpl
to use the new representation.Interface polymorphism: If we aren’t sure which representation is better, or if we suspect that it depends on how it’s being used, we can have both. Because we defined an interface
Duration
before implementing a class, we can create a second class implementingDuration
. Then to the extent that we wrote client code that refers exclusively to the interfaceDuration
rather than specifying the implementing classDurationImpl
, that client code will work unchanged with both representations going forward.
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:
This is a UML class diagram with the following standard notations:
A dashed arrow with open head means
implements
. (A solid arrow with an open head would meanextends
.)Italic type means that the named entity is abstract; in particular, the methods in interface
Duration
are in italics because it declares them but leaves them undefined.In each class, the first section is a list of fields and the second a list of methods.
In the lists of fields and methods, + means
public
and - meansprivate
.
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:
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 Duration
s that we give
HappyClient
can be DurationImpl
s and will work properly. And because the new class, CompactDuration
is a Duration
as well, durations that we use with HappyClient
can be CompactDuration
s 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:
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:
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:
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:
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.