More Typing, Less Testing: TDD with Static Types, Part 2

In part 1 of this post I claimed it’s easy to test-drive our way into a poor design. We looked at some techniques for using types with TDD, but the examples were simple. Today I’ll walk through Kent Beck’s Money example showing where the design fails and how it can be improved with types.

I like TDD and Kent Beck’s book, Test-Driven Development By Example. I owe him a large debt for teaching me this development technique. However, the Money example that the book builds has some problems. It’s unsafe, difficult to use, and resistant to refactoring.

How did this happen? Why didn’t TDD evolve the design in a better direction? I think there are a few reasons:

  • TDD is overly sensitive to the initial test. Many of the problems with the Money design stem from the very first test: $5 + 10 CHF = $10 if rate is 2:1. That test has jumped from a specific requirement of reporting the value of a multi-currency investment, to the general requirement of adding different currencies.
  • The red-green-refactor loop quickly finds a solution, but it doesn’t help find good solutions. Spikes can explore possible solutions, but spikes often result in a patchwork of design ideas that don’t feed cleanly back into the TDD process. Beck and I agree that nothing guarantees we will have flashes of insight at the right moment, however, TDD allows us to keep coding without insight when it might be more valuable to shift our focus to a higher level.
  • Tests legitimize a design too early. There’s a psychological aspect to having lots of tests: we want to believe that a tested design is safe and correct even if it isn’t, so we stick with a design longer than we should. Beck is confident in his test coverage, but there are many unsafe aspects to the design, notably stringy currencies where “usd” or “$” cause run-time errors. The Money class can also be difficult to use because it is impossible to read a piece of code and verify that currency is being handled correctly: a complex feature (currency exchange) has been incorporated into a seemingly simple class.
  • Object-oriented designs often confuse the implementation hierarchy with the type system. This puts artificial pressure on the design to eliminate types, even important domain types. TDD helps in the destruction because it focuses our minds on performing work, not preventing errors. Beck says that pretty clearly, “But because a constructor is not reason enough to have a subclass, we want to delete the subclasses.” This was the last chance to save Money from stringy currencies.

Using Types with Test-Driven Development

So how can we improve TDD to reduce these problems? Add a step 0 to the TDD process:

  1. Define all the types.
  2. Write a test.
  3. Make it run.
  4. Make it right.

Defining types is very much like writing tests–the compiler continuously checks the types for consistency while we loop back and fix errors. Step 0 is exactly like normal TDD, except we are making formal statements about the system that the compiler maintains. Could step 0 take a long time? Sure. Maybe with a sufficiently-advanced type system we never even leave step 0. With Java I’m going to hit a wall pretty fast, but not before avoiding many of the worst problems with the Money design.

Reworking the Money Example

I’m going to jump in where Beck’s Money design grows to a simple class hierarchy:

class Money

class Dollar extends Money
class Franc extends Money

Remember, I’m going to create a lot of types here without any tests. I’m going to lean on the Java compiler to type check everything. I’m never going to write any code that it can’t type check. This limits me to interfaces, classes, simple data-flow operations and method signatures. As soon as I need to do anything else, I’ll switch to writing a test first.

Value vs. currency

The Money design immediately bothers me because real-world money has a value vs currency duality. I want to capture this concern by adding a Currency interface to the design. It might be deleted or not; I’m just thinking out loud playing with types.

interface Currency

class Money implements Currency

class Dollar extends Money
class Franc extends Money

Currencies as siblings

It also bothers me that Dollar and Franc are siblings. The substitution principle says that anywhere I have Money, I can substitute it with either Dollar or Franc. That seems very dangerous! If one of my Ann Arbor friends asks me for some money, and I give them a Franc, they will be confused at best. Is Money a localized type alias? Maybe a type class?

One way to represent that in Java is as a generic type where an instance of Money is bound to a specific Currency.

interface Currency

class Money implements Currency

class Dollar extends Money
class Franc extends Money

But that simple generic type is too loose. This kind of non-sense can be written:

class Hmm extends Money

So I tighten things up:

interface Currency>

class Money> implements Currency

class Dollar extends Money
class Franc extends Money

This is starting to look interesting! It seems to capture the value vs currency duality of money, allows code sharing between Dollars and Francs, yet prevents accidental use of a Franc where a Dollar is expected. I’ll use the convention that Dollar and Franc are currencies and Money<Dollar> and Money<Franc> are money values.

Currency exchange

The application requires currency exchange, specifically between Dollars and Francs, and that isn’t represented anywhere in the types. I need to annotate Dollar and Franc so that they can be exchanged, but prohibit exchange of other currencies:

interface Currency>
interface ExchangableCurrency> extends Currency

class Money> implements Currency

class Dollar extends Money implements ExchangableCurrency
class Franc extends Money implements ExchangableCurrency

class Scrip extends Money

Throwing in Scrip is a design canary–I don’t really care much about it, but it helps me understand how my types cover the problem domain. Again, I’m not sure if this idea will be important, but it’s very cheap to try.

Money from currency

The types seem reasonable so far, but there should be a way to generate money values from a currency. That’s exactly what someone means when they say “10 dollars”. The Currency and Money<Currency> differentiation is starting to look even more useful.

interface Currency> {
    public Money money(double amount);
}

class Money> implements Currency {
    public T currency() { return null; } // type erasure woes
    public Money money(double amount) { return new Money(); }
}

class Dollar extends Money implements ExchangableCurrency {
    public Dollar currency() { return new Dollar(); }
    public Dollar money(double amount) { return new Dollar(); }
}

class Franc extends Money implements ExchangableCurrency {
    public Franc currency() { return new Franc(); }
    public Franc money(double amount) { return new Franc(); }
}

Generating money values

One big problem with the code is that even though the type system differentiates between Currency and Money, there’s nothing useful we can do with Money. Money needs to hold an amount. I’m comfortable doing this without a test (it’s simple data flow in a constructor) because the compiler will force me to fix the immutable type errors. It’s easy to test-drive this code too and you’ll end up at the same place–although I still recommend deleting the tests after you’ve finished the code because there’s nothing gained by the test over what the compiler does.

Here’s the code now with useful Currency and Money types:

interface Currency> {
    public Money money(double amount);
}

class Money> implements Currency {
    final public double amount;
    public T currency() { return null; } // type erasure woes
    public Money(double amount) { this.amount = amount; }
    public Money money(double amount) { return new Money(amount); }
    // standard IDE-generated equals() and hashCode()
}

class Dollar extends Money implements ExchangableCurrency {
    final static Dollar currency = new Dollar(0);
    public Dollar currency() { return currency; }
    public Dollar money(double amount) { return new Dollar(amount); }
    public Dollar(double amount) { super(amount); }
}

class Franc extends Money implements ExchangableCurrency {
    final static Franc currency = new Franc(0);
    public Franc currency() { return currency; }
    public Franc money(double amount) { return new Franc(amount); }
    public Franc(double amount) { super(amount); }
}

There are ugly aspects to the design, and the boiler plate in Dollar and Franc stinks, but all of that seems caused by Java’s type system limitations. The code was type-driven, not test-driven, so I’m willing to live with it if that’s just how the types look in Java. (If anyone has a better type-safe implementation in Java, I’m interested to hear about it. Other languages have a much easier time expressing this. C++ and C# have generics without erasure. It’d be fun to see different languages in the comments.)

Switching from types to tests

Next, lets try implementing basic adding of Dollars. I start with a test because I don’t see a way to express this with pure types:

@Test
public void REPL() {
    assertThat(new Dollar(1).add(new Dollar(2)), is(new Dollar((3))));
}

And then the method:

class Money> implements Currency {
    public Money add(Money other) { return money(amount + other.amount); }
}

But that doesn’t make the test pass. The test doesn’t even compile. Java complains that I’m mixing up Money<Dollar> and Dollar. I made an error mixing up the Money and Currency meaning of Dollar–my test was wrong!

The properly typed test looks like this:

@Test
public void REPL() {
    assertThat(new Money(1).add(new Money(2)), is(new Money(3)));
}

That code passes, but it runs into trouble with type erasure. The compiler will guarantee that Money is used consistently, but the Java run-time will lose the type difference between new Money(2) and new Money(2). That’s seriously bad news for value types in Java.

The fix is to add some sugar for constructing Money values and hide the lower-level constructor. Here’s the test:

@Test
public void REPL() {
    assertThat(dollars(1).add(dollars(2)), is(dollars(3)));
}

and the code that makes it pass:

static Money dollars(double n) { return new Dollar(n); }

Lets verify that Dollars and Francs really don’t mix with a test:

@Test
public void REPL() {
    assertThat(dollars(1).add(francs(2)), is(not(dollars(3))));
}

Even after implementing the sugar for francs(), the test doesn’t compile. The compiler won’t let us mix Dollars and Francs. One exception is caused by Java’s equals() method–it’s type unsafe, but built deep into the run-time. The compiler allows mixing Dollars and Francs here, but value equality will work properly if we are careful to prevent type erasure:

@Test
public void REPL() {
    // assertThat(dollars(1), is(not(francs(1)))); // won't compile
    assertFalse(dollars(1).equals(francs(1)));
}

Back to types quick as we can

The Money and Currency types seem well explored now, so lets try building the multi-currency features required by Beck’s example. I’m going to model a Bank and a bank deposit Account that allows multiple currencies. This is a little different than the book, but the book never delivered the original use cases because it took a detour towards abstract multi-currency expressions.

The Bank will be responsible for exchanging currency, so it keeps track of currency exchange rates. Banks own Accounts. Accounts hold a balance of deposits in various currencies.

Like before, lets just focus on the types without any tests.

class Bank {
    final HashMap exchangeRate = new HashMap<>();
    public Account openAccount() { return new Account(this); }
}

class Account {
    final Bank bank;
    final HashMap> balance = new HashMap<>();
    Account(Bank bank) { this.bank = bank; }
}

class ExchangablePair {
    final public ExchangableCurrency base;
    final public ExchangableCurrency quote;
    public ExchangablePair(ExchangableCurrency base, ExchangableCurrency quote) {
        this.base = base;
        this.quote = quote;
    }
    // standard IDE-generated equals() and hashCode()
}

Tests again

This looks pretty good, and the ExchangableCurrency type is really becoming useful. I’ve hit the wall with what I can define with types though: I need to test-drive a currency exchange.

@Test
public void REPL() {
    Bank bank = new Bank();
    bank.exchangeRate(dollars(1), francs(2));
    assertThat(bank.exchange(dollars(10), Dollar.currency), is(dollars(10)));
    assertThat(bank.exchange(dollars(10), Franc.currency), is(francs(20)));
}

The code to make this pass is straight-forward and can be achieved with the normal TDD red-green-refactor cycle. The gist at the end of this post contains a full working sample so you can play around with my approach. One thing that’s pretty fun working in a REPL loaded with all the types is that the code sometimes almost writes itself–the types constrain the implementation so that it’s impossible to make a mistake.

For comparison, some of Beck’s tests looked like this:

@Test
public void REPL() {
    Expression fiveBucks = Money.dollar(5);
    Expression tenFrancs = Money.franc(10);
    Bank bank = new Bank();
    bank.addRate("CHF", "USD", 2);
    Money result = bank.reduce(fiveBucks.plus(tenFrancs), "USD");
    assertEquals(Money.dollar(10), result);
}

They have surface similarities, but my code is type safe. If you ask the bank for Dollar.currency and try to assign it to Money<Franc>, it won’t compile. You can’t accidentally provide a currency like “usd” that is unknown to the bank. In Beck’s code it’s even unclear what direction the currency conversion rate applies.

I also think that type-driven code based closely on the requirements has given me a better vocabulary. Beck evolved Money and Expression with TDD, but they became abstract, unclear and overly general. Some of the code, like fiveBucks.plus(tenFrancs) seems dangerous–would it make sense in any situation other than immediately reduced by a bank?

If you already practice TDD and use a typed language, I think it’s well worth your time trying TDD with the type-driven step 0 I’ve shown here. If TDD is new to you, I highly recommend reading Beck’s book first and then adding type-driven development once you are familiar with the basic red-green-refactor cycle.

Happy typing!

Complete Sample REPL with Money Design

Yesterday I mentioned how I work in a REPL while doing initial design work. Normally this design would have been long factored out of the REPL and into individual classes and tests, but it’s been convenient to keep everything here as a single gist.


package com.atomicobject.tdd;

import org.junit.Test;

import java.util.HashMap;

import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

public class MoneyTest {

    interface Currency<T extends Currency<T>> {
        public Money<T> money(double amount);
    }

    interface ExchangableCurrency<T extends ExchangableCurrency<T>> extends Currency<T> {
    }

    static public class Money<T extends Currency<T>> implements Currency<T> {
        final public double amount;
        public T currency() { return null; }

        public Money(double amount) { this.amount = amount; }
        public Money<T> money(double amount) { return new Money<T>(amount); }

        public Money<T> add(Money<T> other) { return money(amount + other.amount); }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            Money money = (Money) o;

            if (Double.compare(money.amount, amount) != 0) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode() {
            long temp = Double.doubleToLongBits(amount);
            return (int) (temp ^ (temp >>> 32));
        }
    }

    static public class Dollar extends Money<Dollar> implements ExchangableCurrency<Dollar> {
        final static Dollar currency = new Dollar(0);
        public Dollar currency() { return currency; }
        public Dollar money(double amount) { return new Dollar(amount); }
        public Dollar(double amount) { super(amount); }
    }

    static public class Franc extends Money<Franc> implements ExchangableCurrency<Franc> {
        final static Franc currency = new Franc(0);
        public Franc currency() { return currency; }
        public Franc money(double amount) { return new Franc(amount); }
        public Franc(double amount) { super(amount); }
    }

    static public class Scrip extends Money<Scrip> {
        public Scrip money(double amount) { return new Scrip(amount); }
        public Scrip(double amount) { super(amount); }
    }

    // due to Java's type erasure, we can't new Money<T> directly
    static Money<Dollar> dollars(double n) { return new Dollar(n); }
    static Money<Franc> francs(double n) { return new Franc(n); }
    static Money<Scrip> scrip(double n) { return new Scrip(n); }

    static public class ExchangablePair {
        final public ExchangableCurrency<?> base;
        final public ExchangableCurrency<?> quote;

        public ExchangablePair(ExchangableCurrency<?> base, ExchangableCurrency<?> quote) {
            this.base = base;
            this.quote = quote;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            ExchangablePair exchange = (ExchangablePair) o;

            if (base != null ? !base.equals(exchange.base) : exchange.base != null) {
                return false;
            }
            if (quote != null ? !quote.equals(exchange.quote) : exchange.quote != null) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode() {
            int result = base != null ? base.hashCode() : 0;
            result = 31 * result + (quote != null ? quote.hashCode() : 0);
            return result;
        }
    }

    static public class Bank {
        final HashMap<ExchangablePair, Double> exchangeRate = new HashMap<>();

        public <T extends ExchangableCurrency<T>, U extends ExchangableCurrency<U>>
        void exchangeRate(Money<T> base, Money<U> quote) {
            exchangeRate.put(new ExchangablePair(quote.currency(), base.currency()), base.amount / quote.amount);
            exchangeRate.put(new ExchangablePair(base.currency(), quote.currency()), quote.amount / base.amount);
            exchangeRate.put(new ExchangablePair(base.currency(), base.currency()), 1.0);
            exchangeRate.put(new ExchangablePair(quote.currency(), quote.currency()), 1.0);
        }

        public <T extends ExchangableCurrency<T>, U extends ExchangableCurrency<U>>
        Money<U> exchange(Money<T> base, U quote) {
            double conversion = exchangeRate.get(new ExchangablePair(base.currency(), quote));
            return quote.money(base.amount * conversion);
        }

        public Account openAccount() { return new Account(this); }
    }

    static public class Account {
        final Bank bank;
        final HashMap<ExchangableCurrency, Money<? extends ExchangableCurrency>> balance = new HashMap<>();

        Account(Bank bank) { this.bank = bank; }

        public <T extends ExchangableCurrency<T>>
        void deposit(Money<T> amount) {
            Money<?> subtotal = balance.get(amount.currency());
            if (subtotal == null) {
                balance.put(amount.currency(), amount);
            }
            else {
                balance.put(amount.currency(), amount.money(subtotal.amount + amount.amount));
            }
        }

        public <T extends ExchangableCurrency<T>>
        Money<T> balance(T desired) {
            Money<T> total = desired.money(0);
            for (Money<? extends ExchangableCurrency> subtotal : balance.values()) {
                total = total.add(bank.exchange(subtotal, desired));
            }
            return total;
        }
    }

    @Test
    public void REPL() {
        assertThat(dollars(1).add(dollars(2)), is(dollars(3)));

        // these errors don't compile so can't happen
        // assertThat(dollars(1).add(francs(2)), is(not(dollars(3))));
        // assertThat(dollars(1), is(not(francs(1))));

        // Java equals is broken, so must test. this code shouldn't even compile!
        assertFalse(dollars(1).equals(francs(1)));

        Bank bank = new Bank();
        bank.exchangeRate(dollars(1), francs(2));

        assertThat(bank.exchange(dollars(10), Dollar.currency), is(dollars(10)));

        // scrip is not exchangable so these won't compile
        // bank.exchangeRate(scrip(1), dollars(1));
        // bank.exchangeRate(dollars(1), scrip(1));

        assertThat(bank.exchange(dollars(10), Franc.currency), is(francs(20)));
        assertThat(bank.exchange(francs(20), Dollar.currency), is(dollars(10)));

        // safe interface: currency exchange rate can be given in either order
        bank.exchangeRate(francs(2), dollars(1));

        assertThat(bank.exchange(dollars(10), Franc.currency), is(francs(20)));

        // can't use money (dollars) when expecting a currency (Dollar.currency)
        // assertThat(bank.exchange(francs(20), dollars(1)), is(dollars(10)));

        Account account = bank.openAccount();
        assertThat(account.balance(Dollar.currency), is(dollars(0)));
        assertThat(account.balance(Franc.currency), is(francs(0)));

        // requesting balance in dollars can't be compared to francs
        // assertThat(account.balance(Dollar.currency), is(francs(0)));

        account.deposit(dollars(5));
        assertThat(account.balance(Dollar.currency), is(dollars(5)));
        assertThat(account.balance(Franc.currency), is(francs(10)));

        // scrip cannot be deposited
        // account.deposit(scrip(5));

        account.deposit(dollars(5));
        assertThat(account.balance(Dollar.currency), is(dollars(10)));

        account.deposit(francs(2));
        assertThat(account.balance(Dollar.currency), is(dollars(11)));
        assertThat(account.balance(Franc.currency), is(francs(22)));
    }
}

Type Erasure Woes: Unique Challenges with Java

It would be nice if a correct implementation of currency() could be used instead of the null stub, but this code fails to compile:

class Money> implements Currency {
    public T currency() { return new T(); }
}

Making currency() abstract would force Money<T> to be abstract which severely limits its use, but prevents many of the errors caused by type erasure. That’s worth it in many designs and I should probably make that change to Money<T> because of the danger of type erasure with a value class.

Here’s another example which falls apart in Java because the type T is erased:

interface Currency>

class Money> implements Currency

class Dollar implements Currency
Money amount = new Money<>(10.0);

Implementing Dollar as a Currency<Dollar> (instead of a Money class) is a perfectly reasonable approach in many languages, but it fails on Java due to type erasure. There will only be one class for all Money types so equals() has neither a class nor a currency() to differentiate money.

Conversation
  • lmm says:

    Thanks for this piece; it’s great to see the case for type safety expressed so clearly.

    I don’t like the way our monetary values are currencies; to my mind that should be more of a has-a relationship. So I would probably do something along the lines of your final example: have class Money<T extends Currency> {T currency(); BigDecimal value();}, and then have singletons for the currencies Dollar, Franc etc. (that implement Currency). (I’m thinking in Scala, where singletons are very easy, but you can implement them fairly elegantly in Java by having an enum with a single value). You can then write “new Money(10.0, Dollar.INSTANCE);’ and it’s typesafe and equals-safe (though I’m tempted to say you should avoid using equals() anyway).

    ExchangeableCurrency doesn’t seem to be doing much; to my mind a currency is exchangeable iff a bank is willing to exchange it (though this may represent a philosophical difference). And there are a few places where the type system leaks. There is nothing to stop balance.get(dollars) returning francs, for example. Or I could create class FradulentDollar extends Currency implements ExchangeableCurrency{}; the self-typing isn’t quite as secure as it looks. And of course we would never use Double for real-world currencies.

    Many things would be nicer in Scala. Rather than list them, I decided to write the whole thing. I may have gone a little overboard with the typing: http://scastie.org/7814

    • Ken Fox Ken Fox says:

      Scala’s type system is very expressive and I agree it could clean up some of the Java. I haven’t used Idris yet, but I bet dependent types would allow banks to limit the types of currencies they exchange based on the exchange rates they have.

      ExchangableCurrency definitely doesn’t do much. I’m not sure I would even keep it if I continued to work on the code. It’s only there to provide a design contrast with general currencies and force me to think about when a type constraint applies. I wanted currencies to be open for extension so the design needed to be generally sound.

      Do you follow TDD? If so, how does type design fit into the routine? What testing framework do you use?

      I’ve only done a couple real-world projects in Scala and it’s easy for me to fall back into unsafe habits. I really like specs2 though, so the “replace tests with types” approach is what I end up using–I can usually only see a type once I see a bad test.

      • lmm says:

        I found Idris’ tooling was very immature. Even in Scala it’s possible to limit the types of currencies the bank will exchange based on those that are available in that particular bank (basically, just move ExchangeRateIS into the Bank itself), though expressing it is a little verbose.

        I used to follow fairly strict TDD; now I’m more relaxed about it. I tend to start with an interface and see whether the implementation is obvious; if I already feel confident I’ll get the implementation correct because there’s only really one possible implementation, I’ll write it straight out. If there’s actual logic to be written, then I’m more likely to write tests first.

        I write traditional JUnit; I sometimes use fest-assert for more readable assertions, and EasyMock for mocks (I find it tends to give better error messages than mockito thanks to its explicit replay/verify phases, and I like that mocks are strict-by-default). I don’t like specs2; I think for tests in particular it’s important that the code be very clear and straightforward so that a newcomer can understand it, and as a programmer I find it much easier to see what a JUnit test is doing than what a DSL-based “given/then/when” test is doing.

        (I’m afraid a few angle brackets were eaten in my previous post; the point was that FraudulentDollar extends Currency typed on *Dollar*)

  • Comments are closed.