Let the Type System Work for You

Paul Holser
97 Things
Published in
3 min readFeb 24, 2020

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 floator 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 Strings 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 itemsties 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.

--

--

Paul Holser
97 Things

Software maker, operations research graduate, developer-testing enthusiast