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

Кент Бек
Экстремальное программирование. Разработка через тестирование

Полная версия

9. Потребность в валюте

$5 + 10 CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым (private) членом

Побочные эффекты в классе Dollar?

Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов

5 CHF * 2 = 10 CHF

Дублирование Dollar/Franc

Общие операции equals()

Общие операции times()

Сравнение франков (Franc) и долларов (Dollar)

Валюта?

Нужен ли тест testFrancMultiplication()?

Есть ли в нашем списке задач какой-либо пункт, который помог бы нам избавиться от этих надоедливых подклассов? Что произойдет, если мы попробуем ввести в нашу программу понятие валюты?

Каким образом мы можем реализовать понятие валюты в данный момент? Черт! Опять я говорю ерунду! Вместо того чтобы снова бить себя линейкой по рукам, попробую перефразировать: каким образом мы можем протестировать понятие валюты в данный момент? Слава богу, мои руки спасены.

Возможно, в будущем нам захочется создать специальный класс валюты, применив шаблон «Приспособленец» (Flyweight Factory), чтобы избежать создания лишних объектов. Однако на текущий момент понятие валюты вполне можно реализовать в виде обычных строк:

public void testCurrency() {

assertEquals("USD", Money.dollar(1).currency());

assertEquals("CHF", Money.franc(1).currency());

}

Прежде всего объявим метод currency() в классе Money:

Money

abstract String currency();

Теперь реализуем этот метод в обоих подклассах:

Franc

String currency() {

return "CHF";

}

Dollar

String currency() {

return "USD";

}

Однако хотелось бы иметь одну и ту же реализацию в обоих подклассах, поэтому сохраним идентификатор валюты в специальном поле класса и просто вернем его значение в методе currency(). (Я начинаю делать рефакторинг быстрее, чтобы сэкономить время. Если вам покажется, что я рассказываю слишком быстро, не стесняйтесь попросить меня замедлиться. Постойте-ка, это же книга, – наверное, мне не стоит слишком уж ускоряться.)

Franc

private String currency;

Franc(int amount) {

this.amount = amount;

currency = "CHF";

}

String currency() {

return currency;

}

То же самое сделаем в классе Dollar:

Dollar

private String currency;

Dollar(int amount) {

this.amount = amount;

currency = "USD";

}

String currency() {

return currency;

}

Теперь мы можем переместить объявление переменной и реализацию метода currency() в базовый класс Money, так как в обоих подклассах они одинаковые:

Money

protected String currency;

String currency() {

return currency;

}

Если переместить строковые константы "USD"и "CHF" в статические фабричные методы, конструкторы двух производных классов станут идентичными, в результате мы сможем создать общую реализацию.

Вначале добавим в конструктор дополнительный параметр:

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 + 10 CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым (private) членом

Побочные эффекты в классе Dollar?

Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов

5 CHF * 2 = 10 CHF

Дублирование Dollar/Franc

Общие операции equals()

Общие операции times()

Сравнение франков (Franc) и долларов (Dollar)

Валюта?

Нужен ли тест testFrancMultiplication()?

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

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

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

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

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

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

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

$5 + 10 CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым (private) членом

Побочные эффекты в классе Dollar?

Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов

5 CHF * 2 = 10 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:<Money.Franc@31aebf> but was:<Money.Money@478a43>". Не очень-то информативно. Не так информативно, как нам хотелось бы. Чтобы получить более осмысленное сообщение об ошибке, добавим метод toString():

Money

public String toString() {

return amount + " " + currency;

}

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

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

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

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

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

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

Money

public boolean equals(Object object) {

Money money = (Money) object;

return amount == money.amount

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

}

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

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

В данном случае мы будем действовать консервативно. (Иногда я плюю на все и пишу тест, не обращая внимания на красную полосу, однако я поступаю так, только когда дети уже спят.)

Franc

Money times(int multiplier) {

return new Franc (amount * multiplier, currency);

}

Перед нами снова зеленая полоса. Мы попали в ситуацию, когда объект Franc(10,"CHF") не равен объекту Money(10,"CHF"), хотя нам хотелось бы, чтобы эти объекты были равны. Превращаем наше желание в тест:

public void testDifferentClassEquality() {

assertTrue(new Money(10, "CHF").equals(new Franc(10, "CHF")));

}

Как и ожидалось, тест потерпел неудачу. Код метода equal() должен сравнивать идентификаторы валют, а не имена классов:

Money

public boolean equals(Object object) {

Money money = (Money) object;

return amount == money.amount

&& currency().equals(money.currency());

}

Теперь метод Franc.times() может возвращать значение Money, и все тесты будут по-прежнему успешно выполняться:

Franc

Money times(int multiplier) {

return new Money(amount * multiplier, currency);

}

Сработает ли этот трюк для метода Dollar.times()?

Dollar

Money times(int multiplier) {

return new Money (amount * multiplier, currency);

}

Да! Теперь две реализации абсолютно идентичны, и мы можем переместить их в базовый класс.

Money

Money times(int multiplier) {

return new Money(amount * multiplier, currency);

}

$5 + 10 CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым (private) членом

Побочные эффекты в классе Dollar?

Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов

5 CHF * 2 = 10 CHF

Дублирование Dollar/Franc

Общие операции equals()

Общие операции times()

Сравнение франков (Franc) и долларов (Dollar)

Валюта?

Нужен ли тест testFrancMultiplication()?

Метод умножения там, где ему следует быть, теперь мы готовы удалить ненужные нам производные классы.

В данной главе мы

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

□ добавили в класс отладочный метод toString() без теста;

□ попробовали модифицировать код (заменили тип Franc возвращаемого значения на Money) и обратились к тестам, чтобы узнать, сработает ли это;

□ отменили изменения и написали еще один тест, добились успешного выполнения теста и вновь применили изменения.

11. Корень всего зла

$5 + 10 CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым (private) членом

Побочные эффекты в классе Dollar?

Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов

5 CHF * 2 = 10 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 + 10 CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым (private) членом

Побочные эффекты в классе Dollar?

Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов

5 CHF * 2 = 10 CHF

Дублирование Dollar/Franc

Общие операции equals()

Общие операции times()

Сравнение франков (Franc) и долларов (Dollar)

Валюта?

Нужен ли тест testFrancMultiplication()?

Тест testDifferentClassEquality() служит доказательством того, что, сравнивая объекты, мы сравниваем различные валюты, но не различные классы. Этот тест имеет смысл только в случае, если в программе существует несколько различных классов. Однако мы уже избавились от класса Dollar и намерены точно так же избавиться от класса Franc. Иными словами, в нашем распоряжении останется только один денежный класс: Money. С учетом наших намерений, тест testDifferentClassEquality() оказывается для нас излишней обузой. Мы удалим его, а затем избавимся от класса Franc.

Обратите также внимание, что в программе присутствуют отдельные тесты для проверки умножения франков на доллары. Если заглянуть в код, можно увидеть, что на текущий момент логика метода, реализующего умножение, не зависит от типа валюты (зависимость была бы только в случае, если бы мы использовали два различных класса). То есть мы можем удалить функцию testFrancMultiplication(), не опасаясь, что потеряем уверенность в правильности работы системы.

Итак, в нашем распоряжении единый денежный класс, и мы готовы приступить к реализации сложения.

 

Но сначала подведем итоги. В этой главе мы

□ закончили потрошить производные классы и избавились от них;

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

12. Сложение, наконец-то

$5 + 10 CHF = $10, если курс обмена 2:1

Наступил новый день, и я заметил, что список задач переполнен вычеркнутыми пунктами. Лучше всего переписать оставшиеся не зачеркнутыми пункты в новый свежий список. (Я люблю физически копировать пункты из старого списка в новый список. Если в старом списке много мелких недоделанных задач, вместо того, чтобы копировать их в новый список, я просто добавляю в программу соответствующий код. В результате из-за моей лени куча мелочей, которая могла бы расти со временем, просто исчезает. Используйте свои слабости.)

$5 + 10 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 вы имеете тесты, формирующие вашу уверенность в коде, а также тщательно вылизанный код, – все это является хорошей почвой для возникновения идеи и ее воплощения в реальность.

Итак, что же является решением в нашем случае? Предлагается создать объект, который ведет себя как объект Money, однако соответствует сумме двух объектов Money. Чтобы объяснить эту идею, я пробовал использовать несколько разных метафор. Например, можно рассматривать сумму различных денежных величин как бумажник. В один бумажник можно положить несколько банкнот разных валют и разных достоинств.

Еще одна метафора: выражение. Имеется в виду математическое выражение, например: (2 + 3) * 5. В нашем случае мы имеем дело с денежными величинами, поэтому выражение может быть таким: ($2 + 3 CHF) * 5. Класс Money – это атомарная форма выражения. В результате выполнения любых операций над денежными величинами получается объект класса Expression. Одним из таких объектов может быть объект Sum7. После того как операция (например, сложение нескольких значений в разных валютах) выполнена, полученный объект Expression можно привести к некоторой заданной валюте. Преобразование к некоторой валюте осуществляется на основании набора курсов обмена.

Как выразить эту метафору в виде набора тестов? Прежде всего, мы знаем, к чему мы должны прийти:

public void testSimpleAddition() {

assertEquals(Money.dollar(10), reduced);

}

Переменная reduced – это объект класса Expression, который создан путем применения обменных курсов в отношении объекта Expression, полученного в результате выполнения математической операции. Кто в реальном мире отвечает за применение обменных курсов? Банк. Стало быть, было бы неплохо, если бы мы могли написать

public void testSimpleAddition() {

Money reduced = bank.reduce(sum, "USD");

assertEquals(Money.dollar(10), reduced);

}

(Плохо, когда в одной программе смешиваются две разные метафоры: банк и математическое выражение. Однако сейчас предлагаю не заострять на этом внимания. Сначала воплотим в жизнь то, что запланировали, а затем посмотрим, можно ли улучшить нашу систему с литературно-художественной точки зрения.)

Обратите внимание на важное дизайнерское решение: метод reduce() принадлежит объекту bank. С такой же легкостью мы могли бы написать

…educed = sum.reduce("USD", bank).

Почему ответственным за выполнение операции reduce() сделан именно объект bank? На самом деле ответ следующий: «Это первое, что пришло мне в голову», однако такой ответ нельзя считать удовлетворительным. Почему мне в голову пришло сделать ответственным за выполнение операции reduce() именно объект класса Bank, а не объект класса Expression? Вот что мне известно на текущий момент:

□ Объекты класса Expression, по всей видимости, лежат в самом сердце того, что мы делаем. Я стараюсь делать объекты, являющиеся сердцем системы, как можно менее зависимыми от всего остального мира. Благодаря этому они остаются гибкими в течение длительного времени («гибкие» в данном случае означает «простые для понимания, тестирования и повторного использования»).

□ Я могу предположить, что класс Expression будет нести ответственность за множество операций. Значит, мы должны по возможности освободить этот класс от лишней ответственности и переложить часть ответственности на другие классы там, где это допустимо. В противном случае класс Expression разрастется до неконтролируемых размеров.

Конечно, это всего лишь догадки – этого не достаточно, чтобы принимать какие-либо окончательные решения, однако этого вполне достаточно, чтобы я начал двигаться в избранном направлении. Безусловно, если выяснится, что наша система вполне может обойтись без класса Bank, я переложу ответственность за выполнение метода reduce() на класс Expression. Если мы используем объект bank, значит, его необходимо создать:

public void testSimpleAddition() {

Bank bank = new Bank();

Money reduced = bank.reduce(sum, "USD");

assertEquals(Money.dollar(10), reduced);

}

Сумма двух объектов Money— это объект класса Expression:

public void testSimpleAddition() {

Expression sum = five.plus(five);

Bank bank = new Bank();

Money reduced = bank.reduce(sum, "USD");

assertEquals(Money.dollar(10), reduced);

}

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

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);

}

Что надо сделать, чтобы данный код откомпилировался? Для начала создадим интерфейс Expression (мы могли бы создать класс, однако интерфейс обладает существенно меньшим весом):

Expression

interface Expression

Метод Money.plus() должен возвращать значение типа Expression:

Money

Expression plus(Money addend) {

return new Money(amount + addend.amount, currency):

}

Это означает, что класс Money должен реализовать интерфейс Expression (это очень просто, так как в этом интерфейсе пока что нет ни одной операции):

Money

class Money implements Expression

Кроме того, нам потребуется пустой класс Bank:

Bank

class Bank

Добавим в этот класс заглушку для метода reduce():

Bank

Money reduce(Expression source, String to) {

return null;

}

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

Bank

Money reduce(Expression source, String to) {

return Money.dollar(10);

}

Зеленая полоса! Теперь мы готовы выполнить рефакторинг. Но сначала подведем итоги главы. В этой главе мы

□ вместо большого теста реализовали меньший тест, чтобы добиться быстрого прогресса (вместо операции $5 + 10 CHF ограничились более простой операцией $5 + $5);

□ основательно обдумали возможные метафоры для нашего предполагаемого дизайна;

□ переписали первоначальный тест в свете новой метафоры;

□ как можно быстрее добились компиляции теста;

□ добились успешного выполнения теста;

□ с трепетом посмотрели вперед, оценив объем рефакторинга, который необходим, чтобы сделать реализацию реальной.

7В переводе на русский язык sum – это сумма. – Примеч. пер.
1  2  3  4  5  6  7  8  9  10  11  12  13  14  15  16  17  18 
Рейтинг@Mail.ru