Теперь переместим метод из класса Dollar в класс Money:
Money
public boolean equals(Object object) {
Money money= (Money) object;
return amount == money.amount;
}
Теперь настало время удалить метод Franc.equals(). Прежде всего мы обнаруживаем, что у нас до сих пор нет теста, проверяющего равенство двух объектов класса Franc, – когда мы, особо не раздумывая, дублировали код класса Dollar, мы нагрешили еще больше, чем думали. Поэтому, прежде чем модифицировать код, мы должны написать все необходимые тесты.
В ближайшем будущем, скорее всего, вам придется использовать подход TDD в отношении кода, который не сопровождается достаточным количеством тестов. В отсутствие адекватного набора тестов любой рефакторинг может привести к нарушению работоспособности кода. Иными словами, в ходе рефакторинга можно допустить ошибку, при этом все имеющиеся тесты будут выполняться как ни в чем не бывало. Ошибка может вскрыться слишком поздно, а ее устранение может стоить слишком дорого. Что же делать?
Прежде чем что-либо менять в коде, вы должны написать все тесты, которые кажутся вам необходимыми. Если этого не сделать, рано или поздно, выполняя рефакторинг, вы чего-нибудь поломаете. Код перестанет работать так, как должен. Вы потратите кучу времени на поиск ошибки и сформируете предубеждение против рефакторинга. Если подобный инцидент повторится, вы можете вообще перестать делать рефакторинг. Дизайн начнет деградировать. Вас уволят с работы. От вас уйдет ваша любимая собака. Вы перестанете мыться и чистить зубы. У вас начнется кариес. Чтобы сохранить зубы здоровыми, всегда сначала пишите тесты и только после этого выполняйте рефакторинг.
К счастью, в нашем случае написать тесты совсем несложно. Для этого достаточно скопировать и немножко отредактировать тесты для класса Dollar:
public void testEquality() {
assertTrue(new Dollar(5). equals(new Dollar(5)));
assertFalse(new Dollar(5). equals(new Dollar(6)));
assertTrue(new Franc(5). equals(new Franc(5)));
assertFalse(new Franc(5). equals(new Franc(6)));
}
Снова дублирование. Целых две строчки! Этот грех нам тоже придется искупить. Но чуть позже.
Теперь, когда тесты на месте, мы можем сделать класс Franc производным от класса Money:
Franc
class Franc extends Money {
private int amount;
}
Далее мы можем уничтожить поле amount в классе Franc, так как это значение будет храниться в одноименном поле класса Money:
Franc
class Franc extends Money {
}
Метод Franc.equals() выглядит фактически так же, как и метод Money.equals(). Сделав их абсолютно одинаковыми, мы сможем удалить реализацию этого метода из класса Franc. При этом смысл нашей программы не изменится. Для начала изменим объявление временной переменной:
Franc
public boolean equals(Object object) {
Money franc = (Franc) object;
return amount == franc.amount;
}
После этого изменим операцию преобразования типа:
Franc
public boolean equals(Object object) {
Money franc = (Money) object;
return amount == franc.amount;
}
Теперь, даже не меняя имя временной переменной, можно видеть, что метод получился фактически таким же, как одноименный метод в классе Money. Однако для пущей уверенности переименуем временную переменную:
Franc
public boolean equals(Object object) {
Money money = (Money) object;
return amount == money.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)
Теперь нет никакой разницы между методами Franc.equals() и Money.equals(), и мы можем удалить избыточную реализацию этого метода из класса Franc. Запускаем тесты. Они выполняются успешно.
Что должно происходить при сравнении франков и долларов? Мы рассмотрим этот вопрос в главе 7.
В данной главе мы
• поэтапно переместили общий код из одного класса (Dollar) в суперкласс (Money);
• сделали второй класс (Franc) подклассом общего суперкласса (Money);
• унифицировали две реализации метода equals() и удалили избыточную реализацию в классе Franc.
7. Яблоки и апельсины
$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)
В конце главы 6 перед нами встал интересный вопрос: что будет, если мы сравним франки и доллары? Мы немедленно добавили соответствующий пункт в список предстоящих задач. Нам никак не избавиться от этой мысли. И в самом деле, что произойдет?
public void testEquality() {
assertTrue(new Dollar(5). equals(new Dollar(5)));
assertFalse(new Dollar(5). equals(new Dollar(6)));
assertTrue(new Franc(5). equals(new Franc(5)));
assertFalse(new Franc(5). equals(new Franc(6)));
assertFalse(new Franc(5). equals(new Dollar(5)));
}
Тест завершается неудачей. С точки зрения написанного кода доллары – это франки. Прежде чем у наших швейцарских клиентов глаза вылезут на лоб, давайте попробуем исправить код. Код сравнения двух денежных значений должен убедиться в том, что он не сравнивает доллары с франками. Для этого мы должны проверить классы сравниваемых объектов – два объекта класса Money считаются равными только в том случае, если у них равны значения amount и классы.
public boolean equals(Object object) {
Money money = (Money) object;
return amount == money.amount
&& getClass(). equals(money.getClass());
}
Подобное использование классов, по правде сказать, отдает неприятным запашком. Предпочтительнее было бы использовать критерий из области финансов, а не из области объектов языка Java. Однако