• сделали идентичными две реализации метода times(), для этого мы избавились от вызовов фабричных методов в них, и заменили константы переменными;
• добавили в класс отладочный метод toString() без теста;
• попробовали модифицировать код (заменили тип Franc возвращаемого значения на Money) и обратились к тестам, чтобы узнать, сработает ли это;
• отменили изменения и написали еще один тест, добились успешного выполнения теста и вновь применили изменения.
11. Корень всего зла
$5 + 1 °CHF = $10, если курс обмена 2:1
$5 * 2 = $10
Сделать переменную amount закрытым (private) членом
Побочные эффекты в классе Dollar?
Округление денежных величин?
equals()
hashCode()
Равенство значению null
Равенство объектов
5 CHF * 2 = 1 °CHF
Дублирование Dollar/Franc
Общие операции equals()
Общие операции times()
Сравнение франков (Franc) и долларов (Dollar)
Валюта?
Нужен ли тест testFrancMultiplication()?
Два производных класса, Dollar и Franc, обладают только конструкторами, однако конструктор – это недостаточная причина для создания подкласса. Мы должны избавиться от бесполезных подклассов.
Ссылки на подклассы можно заменить ссылками на суперкласс, не изменив при этом смысл кода. Начнем с класса Franc:
Franc
static Money franc(int amount) {
return new Money (amount, "CHF");
}
Затем перейдем к классу Dollar:
Dollar
static Money dollar(int amount) {
return new Money (amount, "USD");
}
Ссылок на класс Dollar больше нет, поэтому мы можем удалить этот класс. Однако в только что написанном нами тесте есть одна ссылка на класс Franc:
public void testDifferentClassEquality() {
assertTrue(new Money(10, "CHF"). equals(new Franc(10, "CHF")));
}
Если равенство объектов достаточно хорошо протестировано другими тестами, значит, мы можем безбоязненно удалить этот тест. Давайте взглянем на другие тесты:
public void testEquality() {
assertTrue(Money.dollar(5). equals(Money.dollar(5)));
assertFalse(Money.dollar(5). equals(Money.dollar(6)));
assertTrue(Money.franc(5). equals(Money.franc(5)));
assertFalse(Money.franc(5). equals(Money.franc(6)));
assertFalse(Money.franc(5). equals(Money.dollar(5)));
}
Похоже, что все возможные случаи определения равенства достаточно полно охвачены другими тестами. Я даже сказал бы, что тестов слишком много. Мы можем удалить третье и четвертое выражение assert, так как они дублируют первое и второе:
public void testEquality() {
assertTrue(Money.dollar(5). equals(Money.dollar(5)));
assertFalse(Money.dollar(5). equals(Money.dollar(6)));
assertFalse(Money.franc(5). equals(Money.dollar(5)));
}
$5 + 1 °CHF = $10, если курс обмена 2:1
$5 * 2 = $10
Сделать переменную amount закрытым (private) членом
Побочные эффекты в классе Dollar?
Округление денежных величин?
equals()
hashCode()
Равенство значению null
Равенство объектов
5 CHF * 2 = 1 °CHF
Дублирование Dollar/Franc
Общие операции equals()
Общие операции times()
Сравнение франков (Franc) и долларов (Dollar)
Валюта?
Нужен ли тест testFrancMultiplication()?
Тест testDifferentClassEquality() служит доказательством того, что, сравнивая объекты, мы сравниваем различные валюты, но не различные классы. Этот тест имеет смысл только в случае, если в программе существует несколько различных классов. Однако мы уже избавились от класса Dollar и намерены точно так же избавиться от класса Franc. Иными словами, в нашем распоряжении останется только один денежный класс: Money. С учетом наших намерений, тест testDifferentClassEquality() оказывается для нас излишней обузой. Мы удалим его, а затем избавимся от класса Franc.
Обратите также внимание, что в программе присутствуют отдельные тесты для проверки умножения франков на доллары. Если заглянуть в код, можно увидеть, что на текущий момент логика метода, реализующего умножение, не зависит от типа валюты (зависимость была бы только в случае, если бы мы использовали два различных класса). То есть мы можем удалить функцию testFrancMultiplication(), не опасаясь, что потеряем уверенность в правильности работы системы.
Итак, в нашем распоряжении единый денежный класс, и мы готовы приступить к реализации сложения.
Но сначала подведем итоги. В этой главе мы
• закончили потрошить производные классы и избавились от них;
• удалили тесты, которые имели смысл только при использовании старой структуры кода, но оказались избыточными в коде с новой структурой.
12. Сложение, наконец-то
$5 + 1 °CHF = $10, если курс обмена 2:1
Наступил новый день, и я заметил, что список задач переполнен вычеркнутыми пунктами. Лучше всего переписать оставшиеся не зачеркнутыми пункты в новый свежий список. (Я люблю физически копировать пункты из старого списка в новый список. Если в старом списке много мелких недоделанных задач, вместо того, чтобы копировать их в новый список, я просто добавляю в программу соответствующий код. В результате из-за моей лени куча мелочей, которая могла бы расти со временем, просто исчезает. Используйте свои слабости.)
$5 + 1 °CHF = $10, если курс обмена 2:1
$5 + $5 = $10
Пока что я не представляю себе, как можно реализовать смешанное сложение долларов и франков, поэтому предлагаю начать с более простой задачи: $5 + $5 = $10.
public void testSimpleAddition() {
Money sum = Money.dollar(5). plus(Money.dollar(5));
assertEquals(Money.dollar(10), sum);
}
Мы могли бы подделать реализацию, просто вернув значение Money.dollar(10), однако в данном случае реализация кажется очевидной. Давайте попробуем:
Money
Money plus(Money addend) {
return new Money(amount + addend.amount, currency);
}
(Далее я буду ускорять процесс разработки, чтобы сэкономить бумагу и сохранить ваш интерес. Там, где дизайн не очевиден, я буду подделывать реализацию и выполнять рефакторинг. Я надеюсь, что благодаря этому вы увидите, каким образом в TDD выполняется контроль над величиной шагов.)
Сказав, что планирую увеличить скорость, я немедленно замедляю процесс разработки. Однако я не планирую замедлять процесс написания кода, который обеспечивает успешное тестирование. Я планирую замедлить процесс написания самих тестов. Некоторые ситуации и некоторые тесты требуют тщательного обдумывания. Каким образом мы планируем представить арифметику со смешанными валютами? Это как раз тот случай, когда требуется тщательное обдумывание.
Наиболее важное и сложное ограничение, с которым нам приходится иметь дело, заключается в том, что мы не хотим, чтобы код нашей системы знал о существовании каких-либо валют. Нам хотелось бы, чтобы система имела дело с деньгами и не зависела от того, в какой валюте они представлены. Возможная стратегия состоит в том, чтобы немедленно преобразовывать любые денежные значения в некоторую единую валюту (попробуйте угадать, какая валюта является самой любимой у американских программистов). Однако подобное решение не позволит нам с легкостью варьировать соотношения (курсы обмена) между различными валютами.
Вместо этого мы хотели бы найти решение, которое позволило бы нам в удобной форме реализовать механизм обменных курсов и при этом обеспечить запись арифметических выражений в форме, близкой к стандартной арифметической записи.
Решение основано на объектах. Если имеющийся объект ведет себя не так, как нам хотелось бы, мы создаем еще один объект, обладающий точно таким же внешним протоколом, но отличающейся внутренней реализацией. Этот шаблон называется "Самозванец" (Imposter).
Возможно, многим это покажется хиромантией. Каким образом в данной ситуации можно использовать шаблон "Самозванец"? Однако я не собираюсь шутить над вами – не существует формулы, позволяющей генерировать гениальные дизайнерские решения. Решение проблемы было придумано Уордом Каннигемом десятилетие назад. Я еще не встречал человека, который независимо от Уорда придумал бы нечто подобное. К сожалению, методика TDD не гарантирует генерацию гениальных идей. Вместе с тем благодаря TDD вы имеете тесты, формирующие вашу уверенность в коде, а также тщательно вылизанный код, – все это является хорошей почвой для возникновения идеи и ее воплощения в реальность.