Книги: [Классика] [Базы данных] [Internet/WWW] [Сети] [Программирование] [UNIX] [Windows] [Безопасность] [Графика] [Software Engineering] [ERP-системы] [Hardware]
Отрывок
Обмен валюты
$5 + 10 CHF = $10, если курс обмена 2:1
$5 + $5 = $10
Операция $5 + $5 возвращает объект Money
Bank.reduce(Money)
Приведение объекта Money с одновременной конверсией валют
Reduce(Bank,String)
Изменения, перемены, обмены - их объятия заслуживают внимания (особенно если у вас есть книга с фразой в заголовке "в объятиях изменений" (embrace change)). Впрочем, нас заботит простейшая форма обмена - у нас есть два франка и мы хотим получить один доллар. Это звучит как готовый тест:
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() эквивалентность элементов массива?
public void testArrayEquals() {
assertEquals(new Object[] {"abc"}, new Object[] {"abc"});
}
Нет. Тест не сработал. Придется создавать специальный объект, который будет использоваться в качестве ключа
хэш-таблицы:
Pair
private class Pair {
private String from;
private String to;
Pair(String from, String to) {
this.from= from;
this.to= to;
}
}
Мы планируем использовать объекты Pair в качестве ключей, поэтому нам необходимо реализовать методы equals() и hashCode(). Я не собираюсь писать для этого тесты, так как мы разрабатываем код в контексте рефакторинга. Дело в том, что от работоспособности этого кода жестко зависит срабатывание существующих тестов. Если код работает неправильно, существующие тесты не сработают. Однако в случае, если бы я программировал в паре с кем-то, кто плохо представлял бы себе направление дальнейшего движения, или в случае, если бы логика кода была бы несколько более сложной, я несомненно приступил бы к разработке специальных тестов.
Pair
public boolean equals(Object object) {
Pair pair= (Pair) object;
return from.equals(pair.from) && to.equals(pair.to);
}
public int hashCode() {
return 0;
}
0 - ужасное хэш-значение, однако такой метод хэширования легко реализовать, стало быть мы быстрее получим работающий код. Поиск валюты будет выполняться в соответствии с алгоритмом простого линейного поиска. Позже, когда у нас будет множество валют, мы сможем тщательнее проработать этот вопрос, используя реальные данные.
Теперь нам нужно место, в котором мы могли бы хранить значения обменных курсов:
Bank
private Hashtable rates= new Hashtable();
Нам также потребуется метод добавления нового курса обмена:
Bank
void addRate(String from, String to, int rate) {
rates.put(new Pair(from, to), new Integer(rate));
}
А также метод возврата значения обменного курса:
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 + 10 CHF = $10, если курс обмена 2:1
$5 + $5 = $10
Операция $5 + $5 возвращает объект Money
Bank.reduce(Money)
Приведение объекта Money с одновременной конверсией валют
Reduce(Bank,String)
Далее мы переходим к нашему последнему, самому большому тесту, $5 + 10 CHF. В данной главе мы применили несколько важных технологий:
-добавили параметр, который, как мы ожидаем, нам понадобится;
-удалили дублирование между кодом и тестами;
-написали тест (testArrayEquals), чтобы проверить порядок функционирования встроенной операции Java;
-создали вспомогательный закрытый (private) класс, не обладающий собственными тестами;
-допустили ошибку при рефакторинге, написали еще один тест для того, чтобы изолировать проблему.
Начало
Cодержание
Отрывок
[Заказать книгу в магазине "Мистраль"]