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 + 1 °CHF ограничились более простой операцией $5 + $5);
• основательно обдумали возможные метафоры для нашего предполагаемого дизайна;
• переписали первоначальный тест в свете новой метафоры;
• как можно быстрее добились компиляции теста;
• добились успешного выполнения теста;
• с трепетом посмотрели вперед, оценив объем рефакторинга, который необходим, чтобы сделать реализацию реальной.
7 В переводе на русский язык sum – это сумма. – Примеч. пер.
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). (Возможно, в будущем мы оптимизируем специальный случай сложения двух одинаковых валют, однако это произойдет позже.)
Итак, в результате