5. Adding New Code

After having verified that the simple currency case works we move on to multiple currencies.

The problem of mixed currency arithmetic is that there isn't a single exchange rate. To avoid this problem we introduce a MoneyBag which defers exchange rate conversions. For example:

  1. Adding 12 Swiss Francs to 14 US Dollars is represented as a bag containing the two Monies: 12 CHF and 14 USD.

  2. Adding another 10 Swiss francs gives a bag with 22 CHF and 14 USD.

  3. We can later evaluate a MoneyBag with different exchange rates.

A MoneyBag is represented as a list of Monies and provides different constructors to create a MoneyBag:

class MoneyBag {
    private Vector fMonies= new Vector();

    MoneyBag(Money m1, Money m2) {
        appendMoney(m1);
        appendMoney(m2);
    }

    MoneyBag(Money bag[]) {
        for (int i= 0; i < bag.length; i++)
            appendMoney(bag[i]);
    }
}

The method appendMoney is an internal helper method that adds a Money to the list of Moneys and takes care of consolidating Monies with the same currency. MoneyBag also needs an equals method together with a corresponding test. We skip the implementation of equals and only show the testBagEquals method. In a first step we extend the fixture to include two MoneyBags.

protected void setUp() {
    f12CHF= new Money(12, "CHF");
    f14CHF= new Money(14, "CHF");
    f7USD=  new Money( 7, "USD");
    f21USD= new Money(21, "USD");
    fMB1= new MoneyBag(f12CHF, f7USD);
    fMB2= new MoneyBag(f14CHF, f21USD);
}

With this fixture the testBagEquals test case becomes:

public void testBagEquals() {
    Assert.assertTrue(!fMB1.equals(null));
    Assert.assertEquals(fMB1, fMB1);
    Assert.assertTrue(!fMB1.equals(f12CHF));
    Assert.assertTrue(!f12CHF.equals(fMB1));
    Assert.assertTrue(!fMB1.equals(fMB2));
}

Following "code a little, test a little" we run our extended test with JUnit and verify that we are still doing fine. With MoneyBag in hand, we can now fix the add method in Money.

public Money add(Money m) {
    if (m.currency().equals(currency()) )
        return new Money(amount()+m.amount(), currency());
    return new MoneyBag(this, m);
}

As defined above this method will not compile since it expects a Money and not a MoneyBag as its return value. With the introduction of MoneyBag there are now two representations for Moneys which we would like to hide from the client code. To do so we introduce an interface IMoney that both representations implement. Here is the IMoney interface:

interface IMoney {
    public abstract IMoney add(IMoney aMoney);
    //…
}

To fully hide the different representations from the client we have to support arithmetic between all combinations of Moneys with MoneyBags. Before we code on, we therefore define a couple more test cases. The expected MoneyBag results use the convenience constructor shown above, initializing a MoneyBag from an array.

public void testMixedSimpleAdd() { 
    // [12 CHF] + [7 USD] == {[12 CHF][7 USD]} 
    Money bag[]= { f12CHF, f7USD }; 
    MoneyBag expected= new MoneyBag(bag); 
    Assert.assertEquals(expected, f12CHF.add(f7USD)); 
}

The other tests follow the same pattern:

Next, we extend our test suite accordingly:

public static Test suite() {
    TestSuite suite= new TestSuite();
    suite.addTest(new MoneyTest("testMoneyEquals"));
    suite.addTest(new MoneyTest("testBagEquals"));
    suite.addTest(new MoneyTest("testSimpleAdd"));
    suite.addTest(new MoneyTest("testMixedSimpleAdd"));
    suite.addTest(new MoneyTest("testBagSimpleAdd"));
    suite.addTest(new MoneyTest("testSimpleBagAdd"));
    suite.addTest(new MoneyTest("testBagBagAdd"));
    return suite;
}

Having defined the test cases we can start to implement them. The implementation challenge here is dealing with all the different combinations of Money with MoneyBag until we solve the problem.