Lecture 4: Getting (back) into Java, part 2
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 term. Fortunately we can use the compact representation
with minimal or no changes to client code by taking advantage of our
design choices:
Information hiding: Because we made the fields in
DurationImpl
private, client code cannot depend on that representational choice. 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 write 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’ll call 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, and several aspects of it are worth explaning:
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, but often it will make sense to elide those that aren’t relevant to the explanatory goals of the diagram.
So what’s the significance of the structure we’ve designed? 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
.
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
. Because HappyClient
refers to the interface
Duration
rather than a particular class implementing it, we benefit
from interface polymorphism—the ability to use multiple classes that
implement an interface with a client that depends only on the interface.
In reality, it will often be the case that we have a nearly happy client
that limits its mentions of DurationImpl
to constructor expressions,
since constructing an object requires specifying a class. In this case,
we would need to make a limited number of changes to the nearly happy
client to update the constructors. We will see later a way to design a
client to avoid committing even to calling a particular class’s
constructor.
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 write 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. In a larger, more complex class (which is what we’re practicing for), there’s likely to be even more redundancy. We need a way to factor out the functionality common to the two classes.
2 An abstract base class
When we want to factor out redundancy between functions, we write a helper function and call it from the other functions. When we want to factor out redundancy between classes, we write 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.
To get started, we create a 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, this would
cause adding any two classes of Duration
s to return a
DurationImpl
. That probably isn’t a good design, as 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 write fromSeconds
so that it constructs durations
of the right class, as determined by each concrete subclass of
AbstractDuration
. A method like fromSeconds
, whose purpose is to
create (or somehow produce) objects, is called a factory method. When
a superclass doesn’t know what classes of objects to create and wants to
let its subclasses decide, the factory method pattern provides an
answer. 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.
/**
* 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
.
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.