Let the Type System Work for You
In Java, types of variables are static (known at compile time) and manifest (explicitly named in source code). Some developers consider these attributes burdensome, leading to overly verbose code or an adversarial relationship with the compiler. Could they reap benefits greater than the costs if they were to befriend Java’s type system as an ally?
Consider the problem of representing currency amounts in Java. Sadly, many developers first reach for float
or double
to represent such figures. Leaving aside rounding issues and the unrepresentability of some values using floating-point types, callers face several questions about what to expect from methods using currency amounts as parameters:
public class MoneyExchange {
public double convert(
double from,
String fromUnit,
String toUnit) { // ...
}
}
What units of currency are acceptable? To what precision should conversion occur? Who’s responsible for rounding behavior? Does rounding behavior differ by the units involved and/or the direction of the conversion? The method signature answers none of these questions.
Instead, consider this design:
public final class CurrencyUnit {
// the means to obtain USD, EUR, JPY, etc.
}public class MoneyExchange {
public Money convert(Money from, CurrencyUnit to) {
// ...
}
}public class Money {
private final BigDecimal amount;
private final CurrencyUnit unit; public Money(BigDecimal amount, CurrencyUnit unit) {
this.amount = amount;
this.unit = unit;
} // ...
}
Instances of class Money
represent currency amounts in given units. Money
's constructor can reject any amounts/units it wishes. Class CurrencyUnit
decides what units are acceptable, and its users document their intent more precisely than a String
could (any currency might be encoded in a String
, but not all String
s are currency units). Each CurrencyUnit
instance could respond differently to method calls — for example, Japanese yen may offer no decimal digits for display, where US dollars may offer two.
With this design, we know exactly the kinds of values MoneyExchange
manipulates — not any old numbers and strings, but amounts of money in units of currency. Callers cannot misuse non-money values with MoneyExchange
. Also, the custom classes offer loci for new responsibilities as our application grows.
Consider Optional
as another helpful type. Returning it from a method tells callers something about its result — it might be “missing” or undefined. For example, consider this variation of java.util.Collections.max
:
<T> T uniqueMax(
Collection<? extends T> items,
Comparator<? super T> comp)
If items
is empty, max
raises NoSuchElementException
. But is it so exceptional to hand max
an empty collection sometimes?
Suppose that if more than one element of items
ties for maximum, uniqueMax
should indicate that no unique maximum exists. null
seems like a cheap way out, but it places burden on the caller to check for null
.
With this signature:
<T> Optional<T> uniqueMax(Collection<? extends T> items, Comparator<? super T> comp)
uniqueMax
can return Optional.empty()
if items
is empty, or if there is no unique maximum. Callers can anticipate missing values, and react accordingly — possibly using orElse
to give a default value, or orElseThrow
to raise some domain-appropriate exception.
Avoid reflexively reaching for primitives, strings, collections, maps, and null
to represent domain-level concepts on their own. Instead, use custom types to document your system and help callers and readers of your code understand your intent and program more safely.