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


Franc

Franc(int amount, String currency) {

this.amount = amount;

this.currency = "CHF";

}


При этом возникают ошибки в двух местах при обращении к конструктору:


Money

static Money franc(int amount) {

return new Franc(amount, null);

}


Franc

Money times(int multiplier) {

return new Franc(amount * multiplier, null);

}


Постойте-ка! Почему это метод Franc.times() вызывает конструктор вместо фабричного метода? Будем ли мы заниматься этим сейчас или отложим любые связанные с этим модификации на более позднее время? В рамках догмы мы должны оставить посторонние дела на потом – не следует прерывать то, чем мы сейчас занимаемся. Однако на практике я иногда позволяю себе отвлечься, но лишь ненадолго, и ни в коем случае я не разрешаю себе прерывать прерывание (этому правилу научил меня Джим Коплаен – Jim Coplien). В данном случае будет лучше, если мы подчистим метод times(), прежде чем продолжить:


Franc

Money times(int multiplier) {

return Money.franc(amount * multiplier);

}


Теперь фабричному методу можно передать значение «CHF»:


Money

static Money franc(int amount) {

return new Franc(amount,«CHF»);

}


Наконец, мы можем присвоить значение параметра полю класса:

Franc

Franc(int amount, String currency) {

this.amount = amount;

this.currency = currency;

}


Может показаться, что я снова перемещаюсь вперед слишком маленькими шажками. Действительно ли я рекомендую вам работать в таком же темпе? Нет. Я рекомендую вначале научиться работать в таком темпе, а затем самостоятельно определять скорость работы, которая покажется вам наиболее эффективной. Я всего лишь попробовал двигаться вперед большими шагами и на половине дороги допустил глупую ошибку. Запутавшись, я вернулся назад на несколько минут, перешел на пониженную передачу и сделал работу заново, более мелкими шажками. Сейчас я чувствую себя уверенней, поэтому мы можем попробовать внести такие же изменения в класс Dollar за один большой шаг:


Money

static Money dollar(int amount) {

return new Dollar(amount,«USD»);

}


Dollar

Dollar(int amount, String currency) {

this.amount = amount;

this.currency = currency;

}

Money times(int multiplier) {

return Money.dollar(amount * multiplier);

}


И это сработало с первого раза. Классно!

Подобная настройка скорости весьма характерна для TDD. Вам кажется, что слишком маленькие шажки ограничивают вас? Попробуйте двигаться быстрее. Почувствовали неуверенность? Переходите на короткий шаг. TDD – это процесс плавного управления – немного в одну сторону, немного в другую сторону. Не существует одного-единственного наиболее правильного размера шага, ни сейчас, ни в будущем.

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


Money

Money(int amount, String currency) {

this.amount = amount;

this.currency = currency;

}


Franc

Franc(int amount, String currency) {

super(amount, currency);

}


Dollar

Dollar(int amount, String currency) {

super(amount, currency);

}

$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()?


Мы уже почти готовы переместить реализацию times() в базовый класс, но прежде вспомним, что в данной главе мы

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

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

• на короткое время отвлеклись от рефакторинга, чтобы добавить в метод times() вызов фабричного метода;

• выполнили аналогичный рефакторинг в отношении класса Dollar за один большой шаг;

• получили два абсолютно идентичных конструктора и переместили код в базовый класс.

10. Избавление от двух разных версий times()

$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()?


В конце данной главы мы должны получить единый класс Money, соответствующий понятию «деньги». Две реализации метода times() близки друг к другу, однако они не идентичны:


Franc

Money times(int multiplier) {

return Money.franc(amount * multiplier);

}


Dollar

Money times(int multiplier) {

return Money.dollar(amount * multiplier);

}


Увы, я не вижу простого способа добиться идентичности этих методов, однако в некоторых ситуациях, для того чтобы продвинуться дальше, требуется вернуться немного назад, – это напоминает кубик Рубика. Что будет, если мы заменим вызовы фабричных методов операторами new? (Я отлично понимаю, что совсем недавно мы выполнили обратную процедуру – заменили new вызовами фабричных методов. Но что я могу поделать – сейчас мы решаем несколько иную задачу. Понимаю, что это может показаться обескураживающим, однако потерпите немного.)


Franc

Money times(int multiplier) {

return new Franc(amount * multiplier, "CHF");

}


Dollar

Money times(int multiplier) {

return new Dollar(amount * multiplier, "USD");

}


Мы абсолютно уверены, что в экземплярах класса Franc значение поля currency всегда будет равно «CHF», поэтому можем написать:


Franc

Money times(int multiplier) {

return new Franc(amount * multiplier, currency);

}


Сработало! Теперь тот же трюк можно проделать и в отношении класса Dollar:


Dollar

Money times(int multiplier) {

return new Dollar(amount * multiplier,currency);

}


Мы почти закончили. Имеет ли значение, что мы используем в данном случае – Franc или Money? Об этом можно рассуждать в течение некоторого времени исходя из имеющихся знаний о внутреннем устройстве нашей системы, однако у нас есть чистый код и тесты, которые дают нам уверенность в том, что код работает так, как надо. Вместо того чтобы тратить несколько минут на рассуждения, мы можем спросить об этом компьютер. Для этого достаточно внести интересующие нас изменения в код и запустить тесты. Обучая методике TDD, я наблюдаю подобную ситуацию постоянно – опытные умные программисты тратят от 5 до 10 минут на обсуждение вопроса, на который компьютер может дать ответ в течение 15 секунд. Если у вас нет тестов, вам остается только размышлять и предполагать. Если же у вас есть тесты, вместо того, чтобы напрасно тратить время, вы можете провести быстрый эксперимент. Как правило, если у вас есть тесты, быстрее спросить компьютер.

Чтобы провести интересующий нас эксперимент, модифицируем код так, чтобы метод Franc.times() возвращал значение типа Money:


Franc

Money times(int multiplier) {

return new Money (amount * multiplier, currency);

}


В ответ компилятор сообщил, что Money должен быть конкретным (не абстрактным) классом:


Money

class Money

Money times(int amount) {

return null;

}


Получаем красную полоску и сообщение об ошибке: «expected: but was:». Не очень-то информативно. Не так информативно, как нам хотелось бы. Чтобы получить более осмысленное сообщение об ошибке, добавим метод toString():


Money

public String toString() {

return amount + " " + currency;

}


О, ужас! Код без тестов?! Допустимо ли такое? Конечно же, прежде чем писать код метода toString, мы должны были написать соответствующий тест, однако

• мы увидим результаты работы этого метода на экране;

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

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

Обстоятельства приняты к сведению.

Теперь сообщение об ошибке изменилось: "expected:<1 °CHF> but was:<1 °CHF>". Выглядит осмысленней, однако сбивает с толку. В двух объектах хранятся одни и те же данные, однако при этом объекты не считаются равными. Проблема кроется в реализации метода equals():


Money

public boolean equals(Object object) {

Money money = (Money) object;

return amount == money.amount

&& getClass(). equals(money.getClass());

}


В данном случае происходит сравнение имен классов, в то время как логичнее сравнивать идентификаторы валют.

Лучше не писать никаких новых тестов, если перед вами красная полоса. Однако нам нужно внести изменения в разрабатываемый код, и мы не можем изменить код, не обладая соответствующим тестом. Консервативный подход заключается в том, чтобы отменить изменение, которое привело к появлению красной полосы. В этом случае мы вновь получим зеленую полосу. После этого мы сможем модифицировать тест для метода equals(), исправить его реализацию и вновь применить изначальное изменение.