Книга: Экстремальное программирование. Разработка через тестирование
13. Делаем реализацию реальной
13. Делаем реализацию реальной
$5 + 1 °CHF = $10, если курс обмена 2:1
$5 + $5 = $10
Мы не можем вычеркнуть пункт $5 + $5, пока не удалим из кода все повторяющиеся фрагменты. Внимательно рассмотрим код. В нем нет повторяющегося кода, но есть повторяющиеся данные – $10 в «поддельной» реализации:
Bank
Money reduce(Expression source, String to) {
return Money.dollar(10);
}
Это выражение по своей сути дублирует выражение $5 + $5 в коде теста:
public void testSimpleAddition() {
Money five = Money.dollar(5);
Expression sum = five.plus(five);
Bank bank = new Bank();
Money reduced = bank.reduce(sum, "USD");
assertEquals(Money.dollar(10), reduced);
}
Раньше, если у нас имелась «поддельная» реализация, для нас было очевидным, как можно вернуться назад и сформировать реальную реализацию. Для этого достаточно было заменить константы переменными. Однако в данном случае пока не понимаю, как вернуться назад. Поэтому, несмотря на некоторый риск, я решаю двигаться вперед:
$5 + 1 °CHF = $10, если курс обмена 2:1
$5 + $5 = $10
Операция $5 + $5 возвращает объект Money
Прежде всего, метод Money.plus() должен возвращать не просто объект Money, а реальное выражение (Expression), то есть сумму (Sum). (Возможно, в будущем мы оптимизируем специальный случай сложения двух одинаковых валют, однако это произойдет позже.)
Итак, в результате сложения двух объектов Money должен получиться объект класса Sum:
public void testPlusReturnsSum() {
Money five = Money.dollar(5);
Expression result = five.plus(five);
Sum sum = (Sum) result;
assertEquals(five, sum.augend);
assertEquals(five, sum.addend);
}
(Вы когда-нибудь слышали, что в английском языке первое слагаемое обозначается термином augend, а второе слагаемое – термином addend? Об этом не слышал даже автор до тех пор, пока не приступил к написанию данной книги.)
Только что написанный тест, скорее всего, проживет недолго. Дело в том, что он сильно связан с конкретной реализацией разрабатываемой нами операции и мало связан с видимым внешним поведением этой операции. Однако, заставив его работать, мы окажемся на шаг ближе к поставленной цели. Чтобы скомпилировать тест, нам потребуется класс Sum с двумя полями: augend и addend:
Sum
class Sum {
Money augend;
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);
• реализовали код с приведением типов в одном месте, добились успешного выполнения тестов, а затем переместили код туда, где он должен находиться;
• использовали полиморфизм, чтобы избавиться от явной проверки типа (класса).
- 1. Мультивалютные деньги
- 2. Вырождающиеся объекты
- 3. Равенство для всех
- 4. Данные должны быть закрытыми
- 5. Поговорим о франках
- 6. Равенство для всех, вторая серия
- 7. Яблоки и апельсины
- 8. Создание объектов
- 9. Потребность в валюте
- 10. Избавление от двух разных версий times()
- 11. Корень всего зла
- 12. Сложение, наконец-то
- 13. Делаем реализацию реальной
- 14. Обмен валюты
- 15. Смешение валют
- 16. Абстракция, наконец-то!
- 17. Ретроспектива денежного примера
- Области памяти в реальной жизни
- Обстоятельства реальной жизни
- Цели определяют практическую реализацию, поскольку они ассоциируются с сигналами
- Ответственные за реализацию программы на уровне функциональных подразделений
- 3.2. Механизмы «приспособления» фрейма к реальной ситуации
- Кейс из реальной жизни