15. Смешение валют
$5 + 1 °CHF = $10, если курс обмена 2:1
$5 + $5 = $10
Операция $5 + $5 возвращает объект Money
Bank.reduce(Money)
Приведение объекта Money с одновременной конверсией валют
Reduce(Bank,String)
Теперь мы готовы написать тест, с которого все началось, – $5 + 1 °CHF:
public void testMixedAddition() {
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);
}
Именно такой код нам хотелось бы написать. К сожалению, мы сразу же получаем кучу ошибок компиляции. Обобщая код в процессе перехода от Money к Expression, мы оставили много висящих хвостов, на которые я, конечно же, обратил внимание, но решил вас не беспокоить. Теперь настало время заняться устранением дефектов.
Мы не сможем достаточно быстро обеспечить компиляцию предыдущего теста. Как только мы внесем в код первое изменение, нам потребуется внести в код еще изменения, и так далее. Теперь мы можем двигаться дальше одним из двух путей. Мы можем заставить тест работать быстро, для этого надо написать более специфичный тест и затем выполнить обобщение. Второй путь: довериться компилятору и с его помощью найти все ошибки. Давайте попробуем действовать медленно (на практике я внес бы в код все необходимые изменения за один раз).
public void testMixedAddition() {
Money fiveBucks = Money.dollar(5);
Money 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);
}
Тест терпит неудачу. Мы получаем 15 USD вместо 10 USD. Дело в том, что метод Sum.reduce() не выполняет приведение аргументов:
Sum
public Money reduce(Bank bank, String to) {
int amount = augend.amount + addend.amount;
return new Money(amount, to);
}
Если выполнить приведение обоих аргументов, тест должен сработать:
Sum
public Money reduce(Bank bank, String to) {
int amount= augend.reduce(bank, to). amount
+ addend.reduce(bank, to). amount;
return new Money(amount, to);
}
И действительно, тест срабатывает. Теперь мы можем заменить тип Money на тип Expression. Чтобы избежать взаимовлияний, мы начнем издалека и будем двигаться в направлении тестирующего кода. Итак, поля augend и addend теперь могут иметь тип Expression:
Sum
Expression augend;
Expression addend;
Аргументы конструктора тоже могут иметь тип Expression:
Sum
Sum(Expression augend, Expression addend) {
this.augend = augend;
this.addend = addend;
}
(Класс Sum начинает напоминать мне шаблон «Компоновщик» (Composite), однако еще не настолько, чтобы я захотел обобщить его.) С классом Sum, пожалуй, закончили, а что насчет Money?
Аргумент метода plus() может иметь тип Expression:
Money
Expression plus(Expression addend) {
return new Sum(this, addend);
}
Метод times() может возвращать значение типа Expression:
Money
Expression times(int multiplier) {
return new Money(amount * multiplier, currency);
}
Это означает, что операции plus() и times() должны входить в состав интерфейса Expression. С классом Money закончили. Теперь можно изменить аргументы метода plus() в реализации теста:
public void testMixedAddition() {
Money 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);
}
Объект tenFrancs теперь принадлежит типу Expression, а это значит, что мы должны внести в код некоторые изменения. К счастью, компилятор подсказывает нам, что именно мы должны сделать. Прежде всего вносим изменение:
public void testMixedAddition() {
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);
}
Компилятор вежливо сообщает, что plus() не является методом интерфейса Expression. Добавим этот метод в интерфейс:
Expression
Expression plus(Expression addend);
Теперь мы должны добавить этот метод в классы Money и Sum. Money? Да, этот метод должен быть открытым (public) в классе Money:
Money
public Expression plus(Expression addend) {
return new Sum(this, addend);
}
Что касается класса Sum, просто добавим заглушку и отметим необходимость реализации этого метода в списке задач:
Sum
public Expression plus(Expression addend) {
return null;
}
$5 + 1 °CHF = $10, если курс обмена 2:1
$5 + $5 = $10
Операция $5 + $5 возвращает объект Money
Bank.reduce(Money)
Приведение объекта Money с одновременной конверсией валют
Reduce(Bank,String)
Sum.plus
Expression.times
Теперь программа компилируется и все тесты выполняются успешно.
Мы готовы завершить обобщение класса Money до Expression, но прежде, как всегда, подведем краткий итог. В этой главе мы
• за один шаг написали необходимый тест и затем модифицировали его, чтобы добиться успешного его выполнения;
• выполнили обобщение (использовали более абстрактное объявление);
• воспользовались подсказками компилятора, чтобы внести изменения (Expression fiveBucks), которые привели к необходимости дополнительных изменений (добавление метода plus() в интерфейс Expression и т. п.).