Экстремальное программирование. Разработка через тестирование — страница 31 из 41

Еще одно решение предлагает несколько ограничить возможности, которыми обладает типичный объект в рамках ООП. Образно говоря, объект становится «менее чем объектом». Что это значит? Обычные объекты обладают состоянием, которое изменяется с течением времени. Если мы захотим, мы можем запретить им меняться. Если у меня есть объект и я знаю, что он не может измениться, я могу передавать ссылки на этот объект любому другому объекту, не беспокоясь при этом о проблеме наложения имен. Если объект не поддерживает возможности своего изменения, никаких модификаций у меня за спиной не может произойти.

Я помню, как похожая ситуация возникла с целыми числами, когда я впервые изучал язык Smalltalk. Если я изменяю бит 2 на 1, почему все двойки не становятся шестерками?


a:= 2.

b:= a.

a:= a bitAt: 2 put: 1.

a => 6

b => 2


Целые числа – это значения, которые маскируются под объекты. В языке Small-talk это утверждение является истиной для небольших целых чисел и имитируется в случае, если целое число не умещается в машинное слово. Когда я устанавливаю бит, то получаю в свое распоряжение новый объект с установленным битом. Старый объект остается неизменным.

В рамках шаблона «Объект-значение» (Value Object) каждая операция должна возвращать новый объект, а первоначальный объект должен оставаться неизменным. Пользователи должны знать, что они используют объект-значение. В этом случае полученный объект следует сохранить (как в предыдущем примере). Конечно же, из-за необходимости создания новых объектов полученный в результате код может оказаться медленным. Однако в данном случае любые проблемы с производительностью должны решаться в точности так же, как и любые другие проблемы с производительностью: вы должны оценить производительность при помощи тестов с реальными данными, определить, насколько часто производится обращение к медленному коду, выполнить профилирование и определить, какой именно код должен быть оптимизирован и как лучше всего этого достичь.

Я предпочитаю использовать «Объект-значение» (Value Object) в ситуациях, когда операции, выполняемые над объектами, напоминают алгебру. Например, пересечение и объединение геометрических фигур, операции над значениями, с каждым из которых хранится единица измерения, а также операции символьной арифметики. Каждый раз, когда использование «Объект-значение» (Value Object) имеет хоть какой-то смысл, я пытаюсь его использовать, так как результирующий код проще читать и отлаживать.

Все объекты-значения должны реализовать операцию сравнения (а во многих языках подразумевается, что они должны реализовать также операцию хеширования). Если я имею один контракт и другой контракт и они не являются одним и тем же объектом, значит, они не равны. Однако если у меня есть одни пять франков и другие пять франков, для меня не имеет значения тот факт, что это два разных объекта – пять франков и в Африке пять франков – они равны.

Нуль-объект (Null Object)

Как реализовать специальные случаи использования объектов? Создать специальный объект, представляющий собой специальный случай. Специальный объект должен обладать точно таким же протоколом, что и обычный объект, но он должен вести себя специальным образом.

В качестве примера рассмотрим код, который я позаимствовал из java.io.File:


java.io.File

public boolean setReadOnly() {

SecurityManager guard = System.getSecurityManager();

if (guard!= null) {

guard.canWrite(path);

}

return fileSystem.setReadOnly(this);

}


В классе java.io.File можно обнаружить 18 проверок guard!= null. Я преклоняюсь перед усердием, с которым разработчики библиотек Java стараются сделать файлы безопасными для всего остального мира, однако я также начинаю немножко нервничать. Будут ли программисты Oracle и в будущем столь же аккуратны, чтобы не забыть проверить результат выполнения метода getSecurityManager() на равенство значению null?

В рамках альтернативного решения можно создать новый класс LaxSecurity, который вообще не генерирует исключений:


LaxSecurity

public void canWrite(String path) {

}


Если кто-то пытается получить SecurityManager, однако предоставить такой объект нет возможности, вместо него мы возвращаем LaxSecurity:


SecurityManager

public static SecurityManager getSecurityManager() {

return security == null? new LaxSecurity(): security;

}

Теперь мы можем не беспокоиться о том, что кто-то забудет проверить результат выполнения метода на равенство значению null. Изначальный код становится существенно более чистым:


File

public boolean setReadOnly() {

SecurityManager security = System.getSecurityManager();

security.canWrite(path);

return fileSystem.setReadOnly(this);

}

Однажды во время выступления на конференции OOPSLA нас с Эр

ихом Гаммой (Erich Gamma) спросили, можно ли использовать «Нуль-объект» (Null Object) в рамках одного из классов JHotDraw. Я принялся рассуждать о преимуществах такой модернизации, в то время как Эрих посчитал, что для этого нам придется увеличить код на десять строк, при этом мы избавимся от одного условного оператора – преимущество сомнительно. (К тому же аудитория была весьма недовольна нашей несогласованностью.)

Шаблонный метод (Template Method)

Как можно запрограммировать инвариантную последовательность операций, обеспечив при этом возможность модификации или замены отдельных действий в будущем? Напишите реализацию метода исключительно в терминах других методов.

В программировании существует огромное количество классических последовательностей:

• ввод – обработка – вывод;

• отправить сообщение – принять ответ;

• прочитать команду – вернуть результат.

Нам хотелось бы четко и понятно обозначить универсальность этих последовательностей и при этом обеспечить возможность варьирования реализаций каждого из отдельных этапов.

Поддерживаемый любым объектно-ориентированным языком механизм наследования обеспечивает простой способ определения универсальных последовательностей. В суперклассе создается метод, целиком и полностью написанный в терминах других методов. Каждый из подклассов может реализовать эти методы так, как ему удобнее. Например, базовая последовательность выполнения теста определяется в инфраструктуре JUnit следующим образом:


TestCase

public void runBare() throws Throwable {

setUp();

try {

runTest();

}

finally {

tearDown();

}

}


Классы, производные от TestCase, могут реализовать setUp(), runTest() и tearDown() так, как им этого хочется.

При использовании шаблона «Шаблонный метод» (Template Method) возникает вопрос: надо ли создавать для подметодов реализации по умолчанию? В TestCase.runBare() все три подметода обладают реализациями по умолчанию:

• методы setUp() и tearDown() не выполняют никаких операций;

• метод runTest() динамически обнаруживает и запускает все тестовые методы, исходя из имени класса-теста.

Если общая последовательность не имеет смысла, когда не определен один из ее этапов, вы должны отметить это, воспользовавшись любой подходящей возможностью используемого вами языка программирования:

• в Java можно объявить подметод абстрактным;

• в Smalltalk создайте реализацию метода, которая генерирует ошибку SubclassResponsibility.

Я не рекомендую изначально проектировать код так, чтобы в нем использовался шаблонный метод. Лучше всего формировать шаблонные методы исходя из накопленного опыта. Каждый раз, когда я говорю себе: «Ага, вот последовательность, а вот – детали реализации», – позднее я всегда обнаруживаю, что мне приходится переделывать созданный мною шаблонный метод, заново перетасовывая код между общим и частным.

Если вы обнаружили два варианта последовательности в двух подклассах, вы должны попытаться постепенно приблизить их друг к другу. После того как вы отделите различающиеся части и выделите общую часть, то, что останется, и есть шаблонный метод. После этого вы можете переместить шаблонный метод в суперкласс и избавиться от дублирования.

Встраиваемый объект (Pluggable Object)

Как можно выразить несколько разных вариантов поведения кода? Проще всего использовать явный условный оператор:


if(circle) then {

… код, относящийся к circle.

} else {

… код, не относящийся к circle

}


Однако подобный корявый код имеет тенденцию распространяться по всей программе. Если для определения разницы между окружностями и не окружностями вы будете использовать условный оператор хотя бы в одном месте вашего кода, с большой долей уверенности можно сказать, что позднее подобный оператор придется добавить также в другом месте, затем в третьем и т. д.

Вторая по важности задача TDD – устранение дублирования, поэтому вы должны подавить угрозу распространения явных условных операторов в зародыше. Если вы видите, что одно и то же условие проверяется в двух разных местах вашего кода, значит, настало время выполнить базовое объектно-ориентированное преобразование: «Встраиваемый объект» (PluggableObject).

Иногда обнаружить необходимость применения этого шаблона не так просто. Один из самых любимых мною примеров использования встраиваемого объекта был придуман мною и Эрихом Гаммой. Представьте, что мы занимаемся разработкой графического редактора. Если вы когда-нибудь занимались чем-либо подобным, должно быть, вы знаете, что операция выделения объектов обладает несколько усложненной логикой. Если указатель мыши находится над графической фигурой и пользователь нажимает кнопку мыши, значит, последующие перемещения мыши приводят к перемещению фигуры, а при отпускании кнопки мыши выбранная фигура остается на новом месте. Если указатель мыши не находится над какой-либо фигурой, значит, нажав кнопку, пользователь выделяет несколько фигур, последующие перемещения мыши приводят к изменению размера прямоугольника выделения, а при отпускании кнопки мыши фигуры внутри прямоугольника выделения становятся выделенными. Изначальный код выглядит примерно так: