Bank
int rate(String from, String to) {
Integer rate = (Integer) rates.get(new Pair(from, to));
return rate.intValue();
}
Подождите-ка минутку! Перед нами красная полоса. Что случилось? Взглянув на код, мы видим, что проблема в неправильном значении курса при обмене доллара на доллары. Мы ожидаем, что при обмене USD на USD курс обмена будет равен 1, однако на текущий момент это не так. Поскольку эта ситуация стала для нас сюрпризом, оформим ее в виде дополнительного теста:
public void testIdentityRate() {
assertEquals(1, new Bank(). rate("USD", "USD"));
}
Теперь у нас три ошибки, однако все они могут быть исправлены при помощи одного небольшого изменения:
Bank
int rate(String from, String to) {
if (from.equals(to)) return 1;
Integer rate = (Integer) rates.get(new Pair(from, to));
return rate.intValue();
}
Зеленая полоска!
$5 + 1 °CHF = $10, если курс обмена 2:1
$5 + $5 = $10
Операция $5 + $5 возвращает объект Money
Bank.reduce(Money)
Приведение объекта Money с одновременной конверсией валют
Reduce(Bank,String)
Далее мы переходим к нашему последнему, самому большому тесту, $5 + 1 °CHF. В данной главе мы применили несколько важных технологий:
• добавили параметр, который может нам понадобиться;
• удалили дублирование между кодом и тестами;
• написали тест (testArrayEquals), чтобы проверить порядок функционирования встроенной операции Java;
• создали вспомогательный закрытый (private) класс, не обладающий собственными тестами;
• допустили ошибку при рефакторинге и написали еще один тест, чтобы изолировать проблему.
8 Используя игру слов (английское change означает как «изменение», так и «обмен»), автор намекает на свою знаменитую книгу-бестселлер Extreme Programming Explained: Embrace Change. Русский перевод: Бек К. Экстремальное программирование. СПб.: Питер, 2002. 224 с. – Примеч. ред.
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);
}