assertTrue(sum instanceof Money);
}
Тест выглядит несколько неопрятно, так как тестирует внутреннюю реализацию, а не внешнее поведение объектов. Однако он принуждает нас внести в программу изменения, которые нам необходимы, и, в конце концов, это всего лишь эксперимент. Вот код, который мы должны модифицировать, чтобы заставить тест работать:
Money
public Expression plus(Expression addend) {
return new Sum(this, addend);
}
$5 + 1 °CHF = $10, если курс обмена 2:1
$5 + $5 = $10
Операция $5 + $5 возвращает объект Money
Bank.reduce(Money)
Приведение объекта Money с одновременной конверсией валют
Reduce(Bank,String)
Sum.plus
Expression.times
Не существует очевидного и ясного способа проверить валюту аргумента, если этот аргумент является объектом класса Money (по крайней мере, я не могу найти такого способа, однако вы можете над этим подумать). Эксперимент окончился неудачей, мы удаляем тест (который нам все равно не нравился).
Подводим итог. Мы
• написали тест так, чтобы его смысл легко был понят другими программистами, которые в будущем будут читать разработанный нами код;
• наметили эксперимент, призванный сравнить эффективность TDD по отношению к обычному стилю программирования, используемому вами на текущий момент;
• снова столкнулись с необходимостью изменения множества объявлений в разрабатываемом коде и снова воспользовались услугами компилятора, чтобы исправить все неточности;
• попробовали провести быстрый эксперимент, однако отказались от идеи, так как она не сработала, и уничтожили соответствующий тест.
17. Ретроспектива денежного примера
Давайте еще раз окинем взглядом пример реализации мультивалютных вычислений и попробуем оценить использованный нами подход и полученные результаты. Вот несколько тезисов, которых хотелось бы коснуться:
Что дальше? Как определить дальнейшее направление разработки?
Метафора. Впечатляющий эффект, который метафора оказывает на структуру дизайна.
Использование JUnit. Как часто мы запускали тесты и как мы использовали JUnit?
Метрики кода. Численные характеристики получившегося кода.
Процесс. Мы говорим: «красный – зеленый – рефакторинг». Как много усилий прикладывается на каждом из этих этапов?
Качество тестов. Каким образом характеристики тестов TDD соотносятся с характеристиками обычных методик тестирования?
Что дальше?Можно ли считать код завершенным? Нет. Методы Sum.plus() и Money.plus() во многом дублируют друг друга. Если мы преобразуем интерфейс Expression в класс (не совсем обычное преобразование – чаще классы становятся интерфейсами), мы получим возможность переместить общий код в единый общий метод.
На самом деле мне сложно представить себе код, который полностью завершен. Методику TDD можно использовать как способ приближения к идеалу, однако это будет не самое эффективное ее использование. Если вы имеете дело с крупной системой, тогда части системы, с которыми вы работаете каждый день, должны быть тщательно «вылизаны». Иными словами, дизайн должен быть чистым и понятным, а код должен быть хорошо протестированным. В этом случае вы изо дня в день сможете вносить в систему необходимые изменения, не теряя при этом уверенности в работоспособности кода. Однако на периферии системы, где располагаются части, к которым вы обращаетесь относительно редко, количество тестов может быть меньше, а дизайн – уродливее.
Когда я завершаю решение всех очевидных задач, я люблю запускать инструмент проверки оформления кода (например, SmallLint для Smalltalk). Многие полученные в результате этого предложения мне и без того известны. Со многими предложениями я не согласен. Однако автоматизированный инструмент проверки кода ни о чем не забывает, поэтому иногда он обнаруживает то, что было упущено мною из виду.
Еще один полезный вопрос: «Какие дополнительные тесты необходимо написать для системы?» Иногда кажется, что некоторый тест должен потерпеть неудачу, однако, добавив его в тестовый набор, вы обнаруживаете, что он работает. В этом случае необходимо определить, почему так происходит. Иногда тест, который не должен работать, действительно не работает, и вы добавляете его в набор, как признак известного вам ограничения разрабатываемой системы или как напоминание о работе, которую необходимо выполнить позднее.
Наконец, когда список задач пуст, неплохо еще раз проверить дизайн. Удовлетворяет ли данная реализация всем предъявляемым требованиям? Существует ли дублирование, которое сложно устранить при использовании данного дизайна? (Сохранившееся дублирование – признак нереализованного дизайна.)
МетафораЛично для меня самым большим сюрпризом в данном примере явилось впечатляющее отличие окончательного дизайна от тех разработок, с которыми мне приходилось иметь дело до написания этой книги. Я выполнял разработку аналогичного мультивалютного кода для различных программных систем, реально используемых в производстве, по меньшей мере три раза (насколько я могу припомнить). Кроме того, я использовал эту же задачу для разного рода публикаций еще раз шесть или семь. Помимо публикаций я пятнадцать раз программировал этот пример перед аудиторией на различных конференциях (программировал со сцены – звучит здорово, но выглядит менее впечатляюще). Наконец, прежде чем написать окончательный вариант первой части данной книги, я перебрал три или четыре различных направления разработки кода (я менял направление своих мыслей в соответствии с поступавшими ранними рецензиями и отзывами о написанном материале). И вот, пока я работал над текстом первой части, мне в голову пришла мысль использовать в качестве метафоры математические выражения (expressions). В результате дизайн стал развиваться по совершенно иному, не известному мне ранее пути.
Я никогда не думал, что метафора – это настолько мощный инструмент. Многие думают, что метафора – это всего лишь источник имен. Разве не так? Похоже, что нет.
Для представления «комбинации нескольких денежных величин, которые могут быть выражены в разных валютах», Уорд Каннингэм использовал метафору вектора. Имеется в виду математический вектор – набор коэффициентов, каждому из которых соответствует некоторая валюта. Лично я некоторое время использовал метафору суммы денег (MoneySum), затем придумал денежный мешок (MoneyBag) – звучит понятно и близко к реальности, – наконец, остановился на метафоре бумажника (Wallet). Что такое бумажник и как он функционирует, известно абсолютно всем. Все эти метафоры подразумевают, что набор денежных значений (объектов Money) является плоским. Иначе говоря, выражение 2 USD + 5 CHF + 3 USD эквивалентно выражению 5 USD + 5 CHF. Два значения в одной и той же валюте автоматически сливаются в одно.
Метафора математического выражения избавила меня от множества неприятных проблем, связанных со слиянием дублирующихся валют. Результирующий код получился чище, чем я когда-либо видел.