Книга: Экстремальное программирование. Разработка через тестирование
9. Потребность в валюте
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» в статические фабричные методы, конструкторы двух производных классов станут идентичными, в результате мы сможем создать общую реализацию.
Вначале добавим в конструктор дополнительный параметр:
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 за один большой шаг;
• получили два абсолютно идентичных конструктора и переместили код в базовый класс.
- 1. Мультивалютные деньги
- 2. Вырождающиеся объекты
- 3. Равенство для всех
- 4. Данные должны быть закрытыми
- 5. Поговорим о франках
- 6. Равенство для всех, вторая серия
- 7. Яблоки и апельсины
- 8. Создание объектов
- 9. Потребность в валюте
- 10. Избавление от двух разных версий times()
- 11. Корень всего зла
- 12. Сложение, наконец-то
- 13. Делаем реализацию реальной
- 14. Обмен валюты
- 15. Смешение валют
- 16. Абстракция, наконец-то!
- 17. Ретроспектива денежного примера
- 4. Потребность в признании
- 6.3.2. Процедура сбора качественных данных
- Глава 2 Табличный редактор Microsoft Excel
- «Плиточные» структуры и квазикристаллы
- 16.13 Формат сообщений MIME
- Как уразуметь свою техническую роль и придерживаться ее
- Отправка запроса
- Build a Run-Time Image for an OS Design
- Как отвечать на письма, полученные вашей организацией
- 5.1.1. Создание трехмерной модели радиатора
- Привязка к процессорам
- Клонирование по сетке ("Copy to Grid")