Книга: Экстремальное программирование. Разработка через тестирование
Назад: 7. Яблоки и апельсины
Дальше: 9. Потребность в валюте

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();
• добавили объявление метода в общий суперкласс;
• освободили тестовый код от ссылок на производные классы, для этого были созданы фабричные методы;
• заметили, что, когда подклассы исчезли, некоторые тесты стали избыточными, однако никаких действий предпринято не было.
Назад: 7. Яблоки и апельсины
Дальше: 9. Потребность в валюте