Экстремальное программирование. Разработка через тестирование — страница 12 из 41

Money addend;

}


В результате получаем исключение преобразования классов (ClassCastException) – метод Money.plus() возвращает объект Money, но не объект Sum:


Money

Expression plus(Money addend) {

return new Sum(this, addend);

}


Класс Sum должен иметь конструктор:


Sum

Sum(Money augend, Money addend) {

}


Кроме того, класс Sum должен поддерживать интерфейс Expression:


Sum

class Sum implements Expression


Наша система компилируется, однако тесты терпят неудачу – это из-за того, что конструктор класса Sum не присваивает значений полям (мы могли бы создать «поддельную» реализацию, инициализировав поля константами, однако я обещал двигаться быстрее):


Sum

Sum(Money augend, Money addend) {

this.augend = augend;

this.addend = addend;

}


Теперь в метод Bank.reduce() передается объект класса Sum. Если суммируются две одинаковые валюты и целевая валюта совпадает с валютой обоих слагаемых, значит, результатом будет объект класса Money, чье значение будет равно сумме значений двух слагаемых:


public void testReduceSum() {

Expression sum = new Sum(Money.dollar(3), Money.dollar(4));

Bank bank = new Bank();

Money result = bank.reduce(sum, "USD");

assertEquals(Money.dollar(7), result);

}


Я тщательно выбираю значения параметров так, чтобы нарушить работу существующего теста. Когда мы приводим (метод reduce()) объект класса Sum к некоторой валюте, в результате (с учетом упомянутых упрощенных условий) должен получиться объект класса Money, чье значение (amount) совпадает с суммой значений двух объектов Money, переданных конструктору объекта Sum, а валюта (currency) совпадает с валютой обоих этих объектов:


Bank

Money reduce(Expression source, String to) {

Sum sum = (Sum) source;

int amount = sum.augend.amount + sum.addend.amount;

return new Money(amount, to);

}


Код выглядит уродливо по двум причинам:

• мы выполняем приведение к типу Sum, в то время как код должен работать с любым объектом типа Expression;

• мы используем общедоступные поля и два уровня ссылок на поля объектов.

Это достаточно легко исправить. Вначале переместим тело метода в класс Sum и благодаря этому избавимся от лишнего уровня ссылок:


Bank

Money reduce(Expression source, String to) {

Sum sum = (Sum) source;

return sum.reduce(to);

}


Sum

public Money reduce(String to) {

int amount = augend.amount + addend.amount;

return new Money(amount, to);

}


На секундочку заглянем в будущее. Приведение (reduce) суммы к некоторой валюте не может быть выполнено, если объект Sum не знает об обменном курсе. Однако обменный курс хранится в классе Bank, значит, скорее всего, в будущем нам потребуется передавать в метод Sum.reduce() еще один параметр типа Bank. Однако сейчас наш код не требует этого. Поэтому мы не добавляем никаких лишних параметров, чтобы лишний раз в них не путаться. (Что касается меня, то искушение было столь велико, что я все-таки добавил этот параметр, когда в первый раз писал данный код, – мне очень, очень стыдно.)


$5 + 1 °CHF = $10, если курс обмена 2:1

$5 + $5 = $10

Операция $5 + $5 возвращает объект Money

Bank.reduce(Money)


Так, а что же происходит в случае, если аргументом метода Bank.reduce() является объект Money?

Давайте напишем тест, слава богу, перед нами зеленая полоса и мы не видим каких-либо других очевидных способов модификации кода:


public void testReduceMoney() {

Bank bank = new Bank();

Money result = bank.reduce(Money.dollar(1), "USD");

assertEquals(Money.dollar(1), result);

}


Bank

Money reduce(Expression source, String to) {

if (source instanceof Money) return (Money) source;

Sum sum= (Sum) source;

return sum.reduce(to);

}


Какой кошмар! Отвратительно! Тем не менее мы получили зеленую полоску и можем приступать к рефакторингу. Прежде всего, вместо прямой проверки класса всегда следует использовать полиморфизм. Класс Sum реализует метод reduce(String), и, если этот метод добавить в класс Money, мы сможем включить reduce(String) в состав интерфейса Expression.


Bank

Money reduce(Expression source, String to) {

if (source instanceof Money)

return (Money) source.reduce(to);

Sum sum = (Sum) source;

return sum.reduce(to);

}


Money

public Money reduce(String to) {

return this;

}


Включаем метод reduce(String) в состав интерфейса Expression:


Expression

Money reduce(String to);


Теперь можно избавиться от этих уродливых операций приведения типа и проверок классов:


Bank

Money reduce(Expression source, String to) {

return source.reduce(to);

}


Я не вполне доволен ситуацией, когда в интерфейсе Expression и классе Bank присутствуют методы с одинаковыми именами, но с разным набором параметров. Я так и не смог найти приемлемого решения этой проблемы в Java. В языках, где поддерживаются ключевые параметры, разница между методами Bank.reduce(Expression, String) и Expression.reduce(String) делается очевидной благодаря синтаксису языка. Однако в языках, в которых различие параметров определяется различием их позиций в списке параметров, разница между двумя подобными методами становится менее очевидной.


$5 + 1 °CHF = $10, если курс обмена 2:1

$5 + $5 = $10

Операция $5 + $5 возвращает объект Money

Bank.reduce(Money)

Приведение объекта Money с одновременной конверсией валют

Reduce(Bank,String)


Теперь можно приступить к задаче реального обмена одной валюты на другую.

В данной главе мы

• не отметили тест как завершенный, так как не избавились от дублирования;

• чтобы прояснить реализацию, решили двигаться вперед вместо того, чтобы двигаться назад;

• написали тест, чтобы форсировать создание объекта, который, как нам кажется, потребуется в будущем (объект класса Sum);

• ускорили процесс реализации (конструктор класса Sum);

• реализовали код с приведением типов в одном месте, добились успешного выполнения тестов, а затем переместили код туда, где он должен находиться;

• использовали полиморфизм, чтобы избавиться от явной проверки типа (класса).

14. Обмен валюты

$5 + 1 °CHF = $10, если курс обмена 2:1

$5 + $5 = $10

Операция $5 + $5 возвращает объект Money

Bank.reduce(Money)

Приведение объекта Money с одновременной конверсией валют

Reduce(Bank,String)


Изменения, перемены, обмены – их объятия заслуживают внимания (особенно если у вас есть книга с фразой в заголовке «в объятиях изменений» (embrace change))[8]. Впрочем, нас заботит простейшая форма обмена – у нас есть два франка и мы хотим получить один доллар. Это звучит как готовый тест:


public void testReduceMoneyDifferentCurrency() {

Bank bank = new Bank();

bank.addRate("CHF", "USD", 2);

Money result = bank.reduce(Money.franc(2), "USD");

assertEquals(Money.dollar(1), result);

}


Когда я конвертирую франки в доллары, я просто делю значение на два (мы по-прежнему игнорируем все эти неприятные проблемы, связанные с дробными числами). Чтобы сделать полоску зеленой, мы добавляем в код еще одну уродливую конструкцию:


Money

public Money reduce(String to) {

int rate = (currency.equals("CHF") && to.equals("USD"))

? 2

: 1;

return new Money(amount / rate, to);

}


Получается, что класс Money знает о курсе обмена. Это неправильно. Единственным местом, в котором выполняются любые операции, связанные с курсом обмена, должен быть класс Bank. Мы должны передать параметр типа Bank в метод Expression.reduce(). (Вот видите? Мы так и думали, что нам это потребуется. И мы оказались правы.) Вначале меняем вызывающий код:


Bank

Money reduce(Expression source, String to) {

return source.reduce(this, to);

}


Затем меняем код реализаций:


Expression

Money reduce(Bank bank, String to);


Sum

public Money reduce(Bank bank, String to) {

int amount = augend.amount + addend.amount;

return new Money(amount, to);

}


Money

public Money reduce(Bank bank, String to) {

int rate = (currency.equals("CHF") && to.equals("USD"))

? 2

: 1;

return new Money(amount / rate, to);

}


Методы должны быть общедоступными (public), так как все методы интерфейсов должны быть общедоступными (я надеюсь, можно не объяснять, почему).

Теперь мы можем вычислить курс обмена внутри класса Bank:


Bank

int rate(String from, String to) {

return (from.equals("CHF") && to.equals("USD"))

? 2

: 1;

}


И обратиться к объекту bank с просьбой предоставить значение курса обмена:


Money

public Money reduce(Bank bank, String to) {

int rate = bank.rate(currency, to);

return new Money(amount / rate, to);

}


Эта надоедливая цифра 2 снова отсвечивает как в разрабатываемом коде, так и в теле теста. Чтобы избавиться от нее, мы должны создать таблицу обменных курсов в классе Bank и при необходимости обращаться к этой таблице для получения значения обменного курса. Для этой цели мы могли бы воспользоваться хеш-таблицей, которая ставит в соответствие паре валют соответствующий обменный курс. Можем ли мы в качестве ключа использовать двухэлементный массив, содержащий в себе две валюты? Проверяет ли метод Array.equals() эквивалентность элементов массива?