Теперь пришла пора избавиться от дублирующегося кода в методах times(), после этого мы сможем перейти к реализации смешанной валютной арифметики. Однако прежде, чем двинуться дальше, подведем итоги данной главы:
• мы превратили мучающее нас сомнение в тест;
• добились успешного выполнения теста приемлемым, но не идеальным способом – getClass();
• решили не добавлять в программу дополнительной логики, пока у нас не появится более весомая мотивация.
8. Создание объектов
$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)
Валюта?
Две разные реализации метода times() выглядят на удивление похоже:
Franc
Franc times(int multiplier) {
return new Franc(amount * multiplier)
}
Dollar
Dollar times(int multiplier) {
return new Dollar(amount * multiplier)
}
Мы можем сделать их еще более похожими, изменив тип возвращаемого значения на Money:
Franc
Money times(int multiplier) {
return new Franc(amount * multiplier)
}
Dollar
Money times(int multiplier) {
return new Dollar(amount * multiplier)
}
Следующий шаг менее очевиден. Два подкласса, производных от класса Money, мало чем отличаются друг от друга. Возникает желание избавиться от них. Однако мы не можем сделать это за один большой шаг, так как это нельзя будет назвать наглядной демонстрацией методики TDD.
Но что же делать? Полагаю, мы сможем приблизиться к решению задачи об уничтожении подклассов, если избавимся от прямых ссылок на подклассы. Для этого мы можем добавить в класс Money фабричный метод, который возвращал бы объект класса Dollar. Этот метод можно было бы использовать следующим образом:
public void testMultiplication() {
Dollar five = Money.dollar(5);
assertEquals(new Dollar(10), five.times(2));
assertEquals(new Dollar(15), five.times(3));
}
Реализация этого метода создает объект класса Dollar и возвращает его:
Money
static Dollar dollar(int amount) {
return new Dollar(amount);
}
Однако мы хотим избавиться от ссылок на Dollar, поэтому изменим объявление переменной в коде теста:
public void testMultiplication() {
Money five = Money.dollar(5);
assertEquals(new Dollar(10), five.times(2));
assertEquals(new Dollar(15), five.times(3));
}
Компилятор вежливо сообщает нам, что метод times() в классе Money не определен. На текущий момент мы не можем реализовать его, поэтому объявим класс Money абстрактным (может быть, с этого стоило начать?) и объявим также абстрактным метод Money.times():
Money
abstract class Money
abstract Money times(int multiplier);
Теперь мы можем изменить объявление фабричного метода:
Money
static Money dollar(int amount) {
return new Dollar(amount);
}
Все тесты выполняются успешно, стало быть, по крайней мере, мы ничего не сломали. Теперь мы можем использовать фабричный метод повсюду в тестах:
public void testMultiplication() {
Money five = Money.dollar(5);
assertEquals(Money.dollar(10), five.times(2));
assertEquals(Money.dollar(15), five.times(3));
}
public void testEquality() {
assertTrue(Money.dollar(5). equals(Money.dollar(5)));
assertFalse(Money.dollar(5). equals(Money.dollar(6)));
assertTrue(new Franc(5). equals(new Franc(5)));
assertFalse(new Franc(5). equals(new Franc(6)));
assertFalse(new Franc(5). equals(Money.dollar(5)));
}
Теперь мы находимся в несколько более выгодной позиции, чем раньше. Клиентский код ничего не знает о существовании подкласса Dollar. Освободив код тестов от ссылок на подклассы, мы получили возможность изменять структуру наследования, не внося при этом каких-либо изменений в клиентский код.
Прежде чем механически исправлять код теста testFrancMultiplication(), обратите внимание, что теперь он не тестирует никакой логики, кроме той, что уже протестирована функцией testMultiplication(). Напрашивается вопрос: нужна ли нам функция testFrancMultiplication()? Если мы удалим этот тест, потеряем ли мы уверенность в нашем коде? Похоже, что нет, однако мы все же сохраним пока этот тест просто так – на всякий случай.
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)));
}
public void testFrancMultiplication() {
Money five = Money.franc(5);
assertEquals(Money.franc(10), five.times(2));
assertEquals(Money.franc(15), five.times(3));
}
Реализация метода Money.franc() почти такая же, как и реализация метода Money.dollar():
Money
static Money franc(int amount) {
return new Franc(amount);
}
$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();
• добавили объявление метода в общий суперкласс;
• освободили тестовый код от ссылок на производные классы, для этого были созданы фабричные методы;
• заметили, что, когда подклассы исчезли, некоторые тесты стали избыточными, однако никаких действий предпринято не было.
9. Потребность в валюте
$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()?
Есть ли в нашем списке задач какой-либо пункт, который помог бы нам избавиться от этих надоедливых подклассов? Что произойдет, если мы попробуем ввести в нашу программу понятие валюты?
Каким образом мы можем реализовать понятие валюты в данный момент? Черт! Опять я говорю ерунду! Вместо того чтобы снова бить себя линейкой по рукам, попробую перефразировать: каким образом мы можем протестировать понятие валюты в данный момент? Слава богу, мои руки спасены.
Возможно, в будущем нам захочется создать специальный класс валюты, применив шаблон «Приспособленец» (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» в статические фабричные методы, конструкторы двух производных классов станут идентичными, в результате мы сможем создать общую реализацию.
Вначале добавим в конструктор дополнительный параметр: