3. Равенство для всех
Если у меня есть целое число и я прибавляю к нему 1, то не предполагаю, что изменится исходное число, – в результате я ожидаю получить новое число. Объекты же обычно ведут себя иначе. К примеру, если у меня есть контракт и я добавлю 1 к его сумме, это будет означать, что сумма контракта должна измениться (да, несомненно, это пример для обсуждения многих интересных законов бизнеса, которые мы здесь рассматривать не будем).
Мы можем использовать объекты в качестве значений, так же как используем наш объект Dollar. Соответствующий шаблон называется «Объект-значение» (Value Object). Одно из ограничений этого шаблона заключается в том, что значения атрибутов объекта устанавливаются в конструкторе и никогда в дальнейшем не изменяются.
Значительное преимущество использования шаблона «Объект-значение» состоит в том, что не нужно беспокоиться о проблеме наложения имен (aliasing). Скажем, у меня есть объект Check, представляющий собой чек, и я устанавливаю его сумму – $5, а затем присваиваю эти же $5 сумме другого объекта Check. Одна из самых неприятных проблем на моей памяти заключалась в том, что изменение суммы в первом объекте может приводить к непреднамеренному изменению суммы во втором. Это и есть проблема наложения имен.
Используя объекты-значения, не нужно беспокоиться о наложении имен. Если у меня есть пять долларов ($5), они всегда гарантированно будут оставаться именно пятью долларами ($5). Если вдруг кому-то понадобятся $7, придется создать новый объект.
$5 + 1 °CHF = $10, если курс обмена 2:1
$5 * 2 = $10
Сделать переменную amount закрытым членом класса
Побочные эффекты в классе Dollar?
Округление денежных величин?
equals()
Одно из следствий использования шаблона «Объект-значение» заключается в том, что все операции должны возвращать результаты в виде новых объектов, как было покзано в главе 2. Другое следствие заключается в том, что объекты-значения должны реализовывать метод equals(), операцию проверки равенства, потому что одни $5 ничем не отличаются от других.
$5 + 1 °CHF = $10, если курс обмена 2:1
$5 * 2 = $10
Сделать переменную «amount» закрытым членом
Побочные эффекты в классе Dollar?
Округление денежных величин?
equals()
hashCode()
Кроме того, если использовать Dollar в качестве ключа хеш-таблицы, вместе с equals() придется реализовать и hashCode(). Добавим этот пункт в список задач и вернемся к нему, когда это будет необходимо.
Вы ведь не собираетесь немедленно приступать к реализации метода equals()? Отлично, я тоже об этом не думаю. Ударив себя линейкой по руке, я стал размышлять над тем, как протестировать равенство. Для начала $5 должны быть равны $5:
public void testEquality() {
assertTrue(new Dollar(5). equals(new Dollar(5)));
}
Полоска окрасилась красным. Поддельная реализация могла бы просто вернуть значение true:
Dollar
public boolean equals(Object object) {
return true;
}
Конечно, мы с вами знаем, что на самом деле true – это «5 == 5», что, в свою очередь, означает «amount == 5», что соответствует «amount == dollar.amount». Но если бы я сразу проследил все эти шаги, я не смог бы продемонстрировать третью и наиболее консервативную методику реализации – триангуляцию.
Если две станции слежения, находящиеся на известном расстоянии друг от друга, смогут измерить азимут некоторого источника радиосигнала (взять пеленг), этого вполне достаточно, чтобы вычислить местоположение источника радиосигнала (как вы помните из курса тригонометрии, в отличие от меня). Это вычисление и называется триангуляцией.
По аналогии, используя метод триангуляции, мы обобщаем код только в том случае, когда у нас два примера или больше. При этом мы ненадолго игнорируем дублирование между тестом и самим кодом (приложения). Когда второй пример потребует более общего решения, тогда и только тогда мы выполним обобщение.
Итак, для триангуляции нам понадобится второй пример. Как насчет того, чтобы проверить $5!= $6?
public void testEquality() {
assertTrue(new Dollar(5). equals(new Dollar(5)));
assertFalse(new Dollar(5). equals(new Dollar(6)));
}
Теперь необходимо обобщить равенство (equality):
Dollar
public boolean equals(Object object) {
Dollar dollar = (Dollar)object;
return amount == dollar.amount;
}
$5 + 1 °CHF = $10, если курс обмена 2:1
$5 * 2 = $10
Сделать переменную amount закрытым членом класса
Побочные эффекты в классе Dollar?
Округление денежных величин?
equals()
hashCode()
Мы могли бы использовать триангуляцию и для управления обобщением метода times(). Если бы у нас были примеры $5 * 2 = $10 и $5 * 3 = $15, нам не смогли бы просто возвращать константу.
Думаю, триангуляция – довольно интересная вещь. Я использую ее в случае, если не знаю, как выполнять рефакторинг. Если же я представляю, как устранить дублирование между кодом и тестами и создать более общее решение, я просто создаю его. С какой стати я должен создавать еще один тест, если сразу могу выполнить обобщение?
Однако когда когда в голову не приходит ничего умного, триангуляция дает шанс посмотреть на проблему с другой стороны. Сколько степеней свободы вы хотите поддерживать в вашем приложении (какую степень универсальности, другими словами)? Просто попробуйте ввести некоторые из них, и, возможно, ответ станет очевиднее.
$5 + 1 °CHF = $10, если курс обмена 2:1
$5 * 2 = $10
Сделать переменную amount закрытым членом класса
Побочные эффекты в классе Dollar?
Округление денежных величин?
equals()
hashCode()
Равенство значению null
Равенство объектов
Итак, сейчас операция проверки равенства реализована полностью. Но как учесть сравнение со значением null и сравнение c другими объектами? Это часто используемые операции, пока они нам еще не нужны, поэтому мы просто добавим их в список задач.
Теперь, когда у нас есть операция проверки равенства, можно напрямую сравнивать объекты Dollar. Это позволит нам сделать переменную amount закрытой, какой и должна быть добропорядочная переменная экземпляра. Резюмируем все вышесказанное:
• поняли, что для использования шаблона проектирования «Объект-значение» необходимо реализовать операцию проверки равенства;
• создали тест для этой операции;
• реализовали ее простейшим способом;
• продолжили тестирование (вместо того, чтобы сразу приступить к рефакторингу);
• выполнили рефакторинг так, чтобы охватить оба теста сразу.