Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
320 Глава 16 . Переработка SerialDate public abstract boolean isIn(int d, int left, int right); } public boolean isInRange(DayDate d1, DayDate d2, DateInterval interval) { int left = Math.min(d1.getOrdinalDay(), d2.getOrdinalDay()); int right = Math.max(d1.getOrdinalDay(), d2.getOrdinalDay()); return interval.isIn(getOrdinalDay(), left, right); } Мы подошли к концу класса DayDate . Сейчас я еще раз пройдусь по всему классу и напомню, что было сделано . Открывающий комментарий был слишком длинным и неактуальным; я сократил и доработал его [C2] . Затем все оставшиеся перечисления были выделены в отдельные файлы [G12] . Статическая переменная ( dateFormatSymbols ) и три статических метода ( getMonth- Names , isLeapYear , lastDayOfMonth ) были выделены в новый класс с именем DateUtil [G6] . Абстрактные методы были перемещены на более высокий уровень абстракции, где они были более уместными [G24] . Я переименовал Month.make в Month.fromInt [N1] и проделал то же самое для всех остальных перечислений . Для всех перечислений был создан метод доступа toInt() , а поле index было объ- явлено приватным . В plusYears и plusMonths присутствовало дублирование кода [G5], которое мне удалось устранить введением нового метода correctLastDayOfMonth . При этом код всех трех методов стал более понятным . «Волшебное число» 1 [G25] было заменено соответствующей конструкцией Month.JANUARY.toInt() или Day.SUNDAY.toInt() . Я потратил некоторое время на до- работку класса SpreadsheetDate и чистку алгоритмов . Конечный результат пред- ставлен в листингах с Б .7 (с . 442) по Б .16 (с . 451) . Интересно заметить, что покрытие кода в DayDate уменьшилось до 84,9 %! Это объясняется не снижением объема тестируемой функциональности; просто класс сократился, и несколько непокрытых строк имеют больший удельный вес . В классе DayDate тесты покрывают 45 из 53 исполняемых команд . Непокрытые строки настолько тривиальны, что не нуждаются в тестировании . Заключение Мы снова последовали «правилу бойскаута»: код стал немного чище, чем был до нашего прихода . На это потребовалось время, но результат того стоил . Тесто- вое покрытие кода увеличилось, были исправлены некоторые ошибки, код стал 320 Литература 321 чище и компактнее . Хочется верить, что следующему человеку, который будет читать этот код, будет проще в нем разобраться, чем нам . И возможно, этот чело- век сможет сделать этот код еще чище, чем удалось нам . литература [GOF]: Design Patterns: Elements of Reusable Object Oriented Software, Gamma et al ., Addison-Wesley, 1996 . [Simmons04]: Hardcore Java, Robert Simmons, Jr ., O’Reilly, 2004 . [Refactoring]: Refactoring: Improving the Design of Existing Code, Martin Fowler et al ., Addison-Wesley, 1999 . [Beck97]: Smalltalk Best Practice Patterns, Kent Beck, Prentice Hall, 1997 . 321 Запахи и эвристические правила В своей замечательной книге «Refactoring» [Refactporing] Мартин Фаулер опи- сывает много различных «запахов кода» . Следующий список содержит много «запахов», предложенных Мартином, а также ряд моих собственных дополнений . Кроме того, в него были включены некоторые приемы и эвристические правила, которые я часто применяю в своей работе . Чтобы построить этот список, я просмотрел и переработал несколько разных про- грамм . При внесении каждого изменения я спрашивал себя, почему я это делаю, и записывал результат . Так появился довольно длинный список того, что, на мой взгляд, «дурно пахнет» при чтении кода . Предполагается, что вы будете читать список от начала к концу, а также исполь- зовать его как краткий справочник . Обратитесь к приложению В на с . 455, где собраны перекрестные ссылки, указывающие, где в тексте книги упоминалось то или иное эвристическое правило . 17 322 Комментарии 323 Комментарии C1: неуместная информация В комментариях неуместно размещать информацию, которую удобнее хранить в других источниках: в системах управления исходным кодом, в системах контро- ля версий и в других системах протоколирования . Например, история изменений только загромождает исходные файлы длинным историческим и малоинтересным текстом . Метаданные (авторы, дата последней модификации и т . д .) в общем случае также неуместны в комментариях . Комментарии должны быть зарезер- вированы для технической информации о коде и его архитектуре . C2: устаревший комментарий Комментарий, содержимое которого потеряло актуальность, считается устарев- шим . Комментарии стареют довольно быстро . Не пишите комментарии, которые с течением времени устареют . Обнаружив устаревший комментарий, обновите его или избавьтесь от него как можно быстрее . Устаревшие комментарии часто «отрываются» от кода, который они когда-то описывали . Так в вашем коде по- являются плавучие островки недостоверности и бесполезности . C3: Избыточный комментарий Избыточным считается комментарий, описывающий то, что и так очевидно . На- пример: i++; // Увеличение переменной i Или другой пример — комментарий Javadoc, который содержит не больше (а вер- нее, меньше) полезной информации, чем простая сигнатура функции: /** * @param sellRequest * @return * @throws ManagedComponentException */ public SellResponse beginSellItem(SellRequest sellRequest) throws ManagedComponentException Комментарии должны говорить то, что не может сказать сам код . C4: Плохо написанный комментарий Если уж вы беретесь за написание комментария, напишите его хорошо . Не жалей- те времени и позаботьтесь о том, чтобы это был лучший комментарий, который вы способны создать . Тщательно выбирайте слова . Следите за правильностью орфографии и пунктуации . Не пишите сумбурно . Не объясняйте очевидное . Будьте лаконичны . 323 324 Глава 17 . Запахи и эвристические правила C5: Закомментированный код Фрагменты закомментированного кода выводят меня из себя . Кто знает, когда был написан этот код? Кто знает, есть от него какая-нибудь польза или нет? Однако никто не удаляет закомментированный код — все считают, что он пона- добится кому-то другому . Этот код только попусту занимает место, «загнивая» и утрачивая актуальность с каждым днем . В нем вызываются несуществующие функции . В нем исполь- зуются переменные, имена которых давно изменились . В нем соблюдаются ус таревшие конвенции . Он загрязняет модуль, в котором он содержится, и от- влекает людей, которые пытаются его читать . Закомментированный код отвра- тителен! Увидев закомментированный код, удалите его! Не беспокойтесь, система управ- ления исходным кодом его не забудет . Если кому-то этот код действительно понадобится, то он сможет вернуться к предыдущей версии . Не позволяйте за- комментированному коду портить вам жизнь . Рабочая среда e1: Построение состоит из нескольких этапов Построение проекта должно быть одной тривиальной операцией . Без выборки многочисленных фрагментов из системы управления исходным кодом . Без длин- ных серий невразумительных команд или контекстно-зависимых сценариев для построения отдельных элементов . Без поиска дополнительных файлов в формате JAR, XML и других артефактов, необходимых для вашей системы . Сначала вы проверяете систему одной простой командой, а потом вводите другую простую команду для ее построения . svn get mySystem cd mySystem ant all e2: тестирование состоит из нескольких этапов Все модульные тесты должны выполняться всего одной командой . В лучшем случае все тесты запускаются одной кнопкой в IDE . В худшем случае одна про- стая команда вводится в командной строке . Запуск всех тестов — настолько важная и фундаментальная операция, что она должна быть быстрой, простой и очевидной . 324 Разное 325 Функции f1: Слишком много аргументов Функции должны иметь небольшое количество аргументов . Лучше всего, когда аргументов вообще нет; далее следуют функции с одним, двумя и тремя аргу- ментами . Функции с четырьмя и более аргументами весьма сомнительны; ста- райтесь не использовать их в своих программах (см . «Аргументы функций» на с . 64) . f2: Выходные аргументы Выходные аргументы противоестественны . Читатель кода ожидает, что аргумен- ты используются для передачи входной, а не выходной информации . Если ваша функция должна изменять чье-либо состояние, пусть она изменяет состояние объекта, для которого она вызывалась (см . «Выходные аргументы», с . 70) . f3: Флаги в аргументах Логические аргументы явно указывают на то, что функция выполняет более одной операции . Они сильно запутывают код . Исключите их из своих программ (см . «Аргументы-флаги», с . 66) . f4: Мертвые функции Если метод ни разу не вызывается в программе, то его следует удалить . Хранить «мертвый код» расточительно . Не бойтесь удалять мертвые функции . Не забудь- те, что система управления исходным кодом позволит восстановить их в случае необходимости . Разное g1: несколько языков в одном исходном файле Современные среды программирования позволяют объединять в одном исходном файле код, написанный на разных языках . Например, исходный файл на языке Java может содержать вставки XML, HTML, YAML, JavaDoc, English, JavaScript и т . д . Или, скажем, наряду с кодом HTML в файле JSP может присутствовать код Java, синтаксис библиотеки тегов, комментарии на английском языке, ком- ментарии Javadoc, XML, JavaScript и т . д . В лучшем случае результат получается запутанным, а в худшем — неаккуратным и ненадежным . 325 326 Глава 17 . Запахи и эвристические правила В идеале исходный файл должен содержать код на одном — и только одном! — языке . На практике без смешения языков обойтись, скорее всего, не удастся . Но по крайней мере следует свести к минимуму как количество, так и объем кода на дополнительных языках в исходных файлах . g2: Очевидное поведение не реализовано Согласно «принципу наименьшего удивления 1 », любая функция или класс должны реализовать то поведение, которого от них вправе ожидать программист . Допустим, имеется функция, которая преобразует название дня недели в элемент перечисления, представляющий этот день . Day day = DayDate.StringToDay(String dayName); Логично ожидать, что строка "Monday" будет преобразована в Day.MONDAY . Также можно ожидать, что будут поддерживаться стандартные сокращения дней недели, а регистр символов будет игнорироваться . Если очевидное поведение не реализовано, читатели и пользователи кода пере- стают полагаться на свою интуицию в отношении имен функций . Они теряют доверие к автору кода и им приходится разбираться во всех подробностях реа- лизации . g3: некорректное граничное поведение Код должен работать правильно — вроде бы очевидное утверждение . Беда в том, что мы редко понимаем, насколько сложным бывает правильное поведение . Раз- работчики часто пишут функции, которые в их представлении работают, а затем доверяются своей интуиции вместо того, чтобы тщательно проверить работоспо- собность своего кода во всех граничных и особых ситуациях . Усердие и терпение ничем не заменить . Каждая граничная ситуация, каждый необычный и особый случай способны нарушить работу элегантного и интуитив- ного алгоритма . Не полагайтесь на свою интуицию . Найдите каждое граничное условие и напишите для него тест . g4: Отключенные средства безопасности Авария на Чернобыльской станции произошла из-за того, что директор завода отключил все механизмы безопасности, один за другим . Они усложняли проведе- ние эксперимента . Результат — эксперимент так и не состоялся, а мир столкнулся с первой серьезной катастрофой в гражданской атомной энергетике . 1 http://en .wikipedia .org/wiki/Principle_of_least_astonishment 326 Разное 327 Отключать средства безопасности рискованно . Ручное управление serialVersion- UID бывает необходимо, но оно всегда сопряжено с риском . Иногда отключение некоторых (или всех!) предупреждений компилятора позволяет успешно по- строить программу, но при этом вы рискуете бесконечными отладочными сеан- сами . Не отключайте сбойные тесты, обещая себе, что вы заставите их проходить позднее, — это так же неразумно, как считать кредитную карту источником бес- платных денег . g5: Дублирование Это одно из самых важных правил в книге и к нему следует относиться очень серьезно . Практически каждый автор, пишущий о проектировании программного обеспечения, упоминает это правило . Дэйв Томас (Dave Thomas) и Энди Хант (Andy Hunt) назвали его принципом DRY («Don’t Repeat Yourself», то есть «не повторяйтесь») [PRAG] . Кент Бек сделал его одним из основных принципов экс- тремального программирования в формулировке «Один, и только один раз» . Рон Джеффрис (Ron Jeffries) ставит это правило на второе место, после требования о прохождении всех тестов . Каждый раз, когда в программе встречается повторяющийся код, он указывает на упущенную возможность для абстракции . Возможно, дубликат мог бы стать функцией или даже отдельным классом . «Сворачивая» дублирование в подоб- ные абстракции, вы расширяете лексикон языка программирования . Другие программисты могут воспользоваться созданными вами абстрактными концеп- циями . Повышение уровня абстракции ускоряет программирование и снижает вероятность ошибок . Простейшая форма дублирования — куски одинакового кода . Программа выгля- дит так, словно у программиста дрожат руки, и он снова и снова вставляет один и тот же фрагмент . Такие дубликаты заменяются простыми методами . Менее тривиальная форма дублирования — цепочки switch / case или if / else , сно- ва и снова встречающиеся в разных модулях и всегда проверяющие одинаковые наборы условий . Вместо них надлежит применять полиморфизм . Еще сложнее модули со сходными алгоритмами, но содержащие похожих строк кода . Однако дублирование присутствует и в этом случае . Проблема решается применением паттернов ШАБЛОННЫЙ МЕТОД или СТРАТЕГИЯ [GOF] . В сущности, большинство паттернов проектирования, появившихся за последние 15 лет, представляет собой хорошо известные способы борьбы с дублированием . Нормальные формы Кодда устраняют дублирование в схемах баз данных . Само объектно-ориентированное программирование может рассматриваться как стра- тегия модульной организации кода и устранения дубликатов . Естественно, это относится и к структурному программированию . Надеюсь, я достаточно четко выразил свою мысль . Ищите и устраняйте дубли- каты повсюду, где это возможно . 327 328 Глава 17 . Запахи и эвристические правила g6: Код на неверном уровне абстракции В программировании важную роль играют абстракции, отделяющие высокоуров- невые общие концепции от низкоуровневых подробностей . Иногда эта задача решается созданием абстрактных классов, содержащих высокоуровневые кон- цепции, и производных классов, в которых хранятся низко уровневые концепции . Действуя подобным образом, необходимо позаботиться о том, чтобы разделение было полным . Все низкоуровневые концепции должны быть сосредоточены в производных классах, а все высокоуровневые концепции объединяются в ба- зовом классе . Например, константы, переменные и вспомогательные функции, относящиеся только к конкретной реализации, исключаются из базового класса . Базовый класс не должен ничего знать о них . Правило также относится к исходным файлам, компонентам и модулям . Ка- чественное проектирование требует, чтобы концепции разделялись на разных уровнях и размещались в разных контейнерах . Иногда такими контейнерами являются базовые и производные классы; в других случаях это могут быть ис- ходные файлы, модули или компоненты . Но какое бы решение ни было выбрано в конкретном случае, разделение должно быть полным . Высокоуровневые и низ- коуровневые концепции не должны смешиваться . Рассмотрим следующий фрагмент: public interface Stack { Object pop() throws EmptyException; void push(Object o) throws FullException; double percentFull(); class EmptyException extends Exception {} class FullException extends Exception {} } Функция percentFull находится на неверном уровне абстракции . Существует много реализаций стека, в которых концепция заполнения выглядит разумно, однако другие реализации могут не знать, до какой степени заполнен стек . Сле- довательно, эта функция должна располагаться в производном интерфейсе — на- пример, BoundedStack Возможно, вы думаете, что для неограниченного стека реализация может просто вернуть 0? Проблема в том, что абсолютно неограниченного стека не существу- ет . Вам не удастся предотвратить исключение OutOfMemoryException , проверив условие stack.percentFull() < 50.0. Если ваша реализация функции возвращает 0, то она попросту врет . Суть в том, что ложь и фикции не способны компенсировать неверного раз- мещения абстракций . Разделение абстракций — одна из самых сложных задач, решаемых разработчиками . Если выбор сделан неверно, не надейтесь, что вам удастся найти простое обходное решение . 328 Разное 329 g7: Базовые классы, зависящие от производных Самая распространенная причина для разбиения концепций на базовые и про- изводные классы состоит в том, чтобы концепции базового класса, относящиеся к более высокому уровню, были независимы от низкоуровневых концепций производных классов . Следовательно, когда в базовом классе встречаются упо- минания имен производных классов, значит, в проектировании что-то сделано не так . В общем случае базовые классы не должны ничего знать о своих произ- водных классах . Конечно, у этого правила имеются свои исключения . Иногда количество произ- водных классов жестко фиксировано, а в базовом классе присутствует код для выбора между производными классами . Подобная ситуация часто встречается в реализациях конечных автоматов . Однако в этом случае между базовым и про- изводными классами существует жесткая привязка, и они всегда размещаются вместе в одном файле jar . В общем случае нам хотелось бы иметь возможность размещения производных и базовых классов в разных файлах jar . Размещение производных и базовых классов в разных файлах jar, при котором базовые файлы jar ничего не знают о содержимом производных файлов jar, по- зволяет организовать развертывание систем в формате дискретных, независимых компонентов . Если в такие компоненты будут внесены изменения, то они раз- вертываются заново без необходимости повторного развертывания базовых ком- понентов . Такая архитектура значительно сокращает последствия от вносимых изменений и упрощает сопровождение систем в условиях реальной эксплуатации . g8: Слишком много информации Хорошо определенные модули обладают компактными интерфейсами, позво- ляющими сделать много минимальными средствами . Для плохо определенных модулей характерны широкие, глубокие интерфейсы, которые заставляют поль- зователя выполнять много разных операций для решения простых задач . Хорошо определенный интерфейс предоставляет относительно небольшое количество функций, поэтому степень логической привязки при его использовании отно- сительно невелика . Плохо определенный интерфейс предоставляет множество функций, которые необходимо вызывать, поэтому его использование сопряжено с высокой степенью логической привязки . Хорошие разработчики умеют ограничивать интерфейсы своих классов и моду- лей . Чем меньше методов содержит класс, тем лучше . Чем меньше переменных известно функции, тем лучше . Чем меньше переменных экземпляров содержит класс, тем лучше . Скрывайте свои данные . Скрывайте вспомогательные функции . Скрывайте кон- станты и временные переменные . Не создавайте классы с большим количеством методов или переменных экземпляров . Не создавайте большого количества за- щищенных переменных и функций в субклассах . Сосредоточьтесь на создании 329 330 Глава 17 . Запахи и эвристические правила очень компактных, концентрированных интерфейсов . Сокращайте логические привязки за счет ограничения информации . g9: Мертвый код Мертвым кодом называется код, не выполняемый в ходе работы программы . Он содержится в теле команды if , проверяющей невозможное условие . Он со- держится в секции catch для блока try , никогда не инициирующего исключения . Он содержится в маленьких вспомогательных методах, которые никогда не вы- зываются, или в никогда не встречающихся условиях switch / case Мертвый код плох тем, что спустя некоторое время он начинает «плохо пахнуть» . Чем древнее код, тем сильнее и резче запах . Дело в том, что мертвый код не об- новляется при изменении архитектуры . Он компилируется, но не соответствует более новым конвенциям и правилам . Он был написан в то время, когда система была другой . Обнаружив мертвый код, сделайте то, что положено делать в таких случаях: достойно похороните его . Удалите его из системы . g10: Вертикальное разделение Переменные и функции должны определяться вблизи от места их использования . Локальные переменные должны объявляться непосредственно перед первым использованием и должны обладать небольшой вертикальной областью види- мости . Объявление локальной переменной не должно отдаляться от места ее использования на сотню строк . Приватные функции должны определяться сразу же после первого использо- вания . Приватные функции принадлежат области видимости всего класса, но вертикальное расстояние между вызовами и определениями все равно должно быть минимальным . Приватная функция должна обнаруживаться простым про- смотром кода от места первого использования . g11: непоследовательность Если некая операция выполняется определенным образом, то и все похожие операции должны выполняться так же . Это правило возвращает нас к «принципу наименьшего удивления» . Ответственно подходите к выбору новых схем и обо- значений, а если уж выбрали — продолжайте следовать им . Если в функцию включена переменная response для хранения данных HttpServ- letResponse , будьте последовательны и используйте такое же имя переменной в других функциях, работающих с объектами HttpServletResponse . Если метод называется processVerificationRequest , присваивайте похожие имена (например, processDeletionRequest ) методам, обрабатывающим другие запросы . Последовательное соблюдение подобных схем и правил существенно упрощает чтение и модификацию кода . 330 Разное 331 g12: Балласт Какой прок от конструктора по умолчанию, не имеющего реализации? Он только попусту загромождает код . Неиспользуемые переменные, невызываемые функ- ции, бессодержательные комментарии — все это бесполезный балласт, который следует удалить . Поддерживайте чистоту в своих исходных файлах, следите за их структурой и не допускайте появления балласта . g13: Искусственные привязки То, что не зависит друг от друга, не должно объединяться искусственными при- вязками . Например, обобщенные перечисления не должны содержаться в более конкретных классах, потому что в этом случае информация о конкретном клас- се должна быть доступна в любой точке приложения, в которой используется перечисление . То же относится и к статическим функциям общего назначения, объявляемым в конкретных классах . В общем случае искусственной считается привязка между двумя модулями, не имеющая явной, непосредственной цели . Искусственная привязка возникает в результате размещения переменной, константы или функции во временно удоб- ном, но неподходящем месте . Главные причины для появления искусственных привязок — лень и небрежность . Не жалейте времени — разберитесь, где должно располагаться объявление той или иной функции, константы или переменной . Слишком часто мы размещаем их в удобном месте «под рукой», а потом оставляем там навсегда . g14: Функциональная зависть Это один из «запахов кода», описанных у Мартина Фаулера [Refactoring] . Для методов класса должны быть важны переменные и функции того класса, кото- рому они принадлежат, а не переменные и функции других классов . Когда метод использует методы доступа другого объекта для манипуляций с его данными, то он завидует области видимости класса этого объекта . Он словно мечтает нахо- диться в другом классе, чтобы иметь прямой доступ к переменным, с которыми он работает . Пример: public class HourlyPayCalculator { public Money calculateWeeklyPay(HourlyEmployee e) { int tenthRate = e.getTenthRate().getPennies(); int tenthsWorked = e.getTenthsWorked(); int straightTime = Math.min(400, tenthsWorked); int overTime = Math.max(0, tenthsWorked - straightTime); int straightPay = straightTime * tenthRate; int overtimePay = (int)Math.round(overTime*tenthRate*1.5); return new Money(straightPay + overtimePay); } } 331 332 Глава 17 . Запахи и эвристические правила Метод calculateWeeklyPay обращается к объекту HourlyEmployee за данными для обработки . Метод calculateWeeklyPay завидует области видимости HourlyEmployee Он «желает» получить доступ к внутренней реализации HourlyEmployee В общем случае от функциональной зависти следует избавиться, потому что она предоставляет доступ к «внутренностям» класса другому классу . Впрочем, иногда функциональная зависть оказывается неизбежным злом . Рассмотрим следующий пример: public class HourlyEmployeeReport { private HourlyEmployee employee ; public HourlyEmployeeReport(HourlyEmployee e) { this.employee = e; } String reportHours() { return String.format( «Name: %s\tHours:%d.%1d\n», employee.getName(), employee.getTenthsWorked()/10, employee.getTenthsWorked()%10); } } Очевидно, метод reportHours завидует классу HourlyEmployee . С другой стороны, мы не хотим, чтобы класс HourlyEmployee знал о формате отчета . Перемеще- ние форматной строки в класс HourlyEmployee нарушает некоторые принципы объектно-ориентированного проектирования 1 . Такое размещение привязывает HourlyEmployee к формату отчета и делает его уязвимым для изменений в этом формате . g15: Аргументы-селекторы Ничто так не раздражает, как висящий в конце вызова функции аргумент false Зачем он здесь? Что изменится, если этот аргумент будет равен true ? Смысл селектора трудно запомнить, но дело не только в этом — селектор указывает на объединение нескольких функций в одну . Аргументы-селекторы помогают ленивому программисту избежать разбиения большой функции на несколько меньших . Пример: public int calculateWeeklyPay(boolean overtime) { int tenthRate = getTenthRate(); int tenthsWorked = getTenthsWorked(); int straightTime = Math.min(400, tenthsWorked); int overTime = Math.max(0, tenthsWorked - straightTime); 1 А конкретно — принцип единой ответственности, принцип открытости/закрытости и принцип сокрытия реализаций . См . [PPP] . 332 Разное 333 int straightPay = straightTime * tenthRate; double overtimeRate = overtime ? 1.5 : 1.0 * tenthRate; int overtimePay = (int)Math.round(overTime*overtimeRate); return straightPay + overtimePay; } Функция вызывается с аргументом true при оплате сверхурочной работы по по- луторному тарифу или с аргументом false при оплате по стандартному тарифу . Каждый раз, когда вы встречаете вызов calculateWeeklyPay(false) , вам приходится вспоминать, что он означает, и это само по себе неприятно . Но по-настоящему плохо то, что автор поленился использовать решение следующего вида: public int straightPay() { return getTenthsWorked() * getTenthRate(); } public int overTimePay() { int overTimeTenths = Math.max(0, getTenthsWorked() - 400); int overTimePay = overTimeBonus(overTimeTenths); return straightPay() + overTimePay; } private int overTimeBonus(int overTimeTenths) { double bonus = 0.5 * getTenthRate() * overTimeTenths; return (int) Math.round(bonus); } Конечно, селекторы не обязаны быть логическими величинами . Это могут быть элементы перечислений, целые числа или любые другие типы аргументов, в за- висимости от которых выбирается поведение функции . В общем случае лучше иметь несколько функций, чем передавать функции признак для выбора пове- дения . g16: непонятные намерения Код должен быть как можно более выразительным . Слишком длинные выра- жения, венгерская запись, «волшебные числа» — все это скрывает намерения автора . Например, приводившаяся ранее функция overTimePay могла бы выглядеть и так: public int m_otCalc() { return iThsWkd * iThsRte + (int) Math.round(0.5 * iThsRte * Math.max(0, iThsWkd - 400) ); } Такая запись выглядит компактной и плотной, но разбираться в ней — сущее мучение . Не жалейте времени на то, чтобы сделать намерения своего кода мак- симально прозрачными для читателей . 333 334 Глава 17 . Запахи и эвристические правила g17: неверное размещение Одно из самых важных решений, принимаемых разработчиком, — выбор места для размещения кода . Например, где следует объявить константу PI ? В классе Math ? А может, ей место в классе Trigonometry ? Или в классе Circle ? В игру вступает принцип наименьшего удивления . Код следует размещать там, где читатель ожидает его увидеть . Константа PI должна находиться там, где объ- являются тригонометрические функции . Константа OVERTIME_RATE объявляется в классе HourlyPayCalculator Иногда мы пытаемся «творчески» подойти к размещению функциональности . Мы размещаем ее в месте, удобном для нас, но это не всегда выглядит естествен- но для читателя кода . Предположим, потребовалось напечатать отчет с общим количеством отработанных часов . Мы можем просуммировать часы в коде, пе- чатающем отчет, или же накапливать сумму в коде обработки учетных карточек рабочего времени . Чтобы принять решение, можно посмотреть на имена функций . Допустим, в модуле отчетов присутствует функция с именем getTotalHours , а в модуле об- работки учетных карточек присутствует функция saveTimeCard . Какая из этих двух функций, если судить по имени, наводит на мысль о вычислении суммы? Ответ очевиден . Очевидно, по соображениям производительности сумму правильнее вычислять при обработке карточек, а не при печати отчета . Все верно, но этот факт должен быть отражен в именах функций . Например, в модуле обработки учетных карто- чек должна присутствовать функция computeRunningTotalOfHours g18: неуместные статические методы Math.max(double a, double b) — хороший статический метод . Он работает не с од- ним экземпляром; в самом деле, запись вида new Math().max(a,b) или даже a.max(b) выглядела бы довольно глупо . Все данные, используемые max , берутся из двух аргументов, а не из некоего объекта-«владельца» . А главное, что метод Math.max почти наверняка не потребуется делать полиморфным . Но иногда мы пишем статические функции, которые статическими быть не должны . Пример: HourlyPayCalculator.calculatePay(employee, overtimeRate) Эта статическая функция тоже выглядит вполне разумно . Она не работает ни с каким конкретным объектом и получает все данные из своих аргументов . Од- нако нельзя исключать, что эту функцию потребуется сделать полиморфной . Возможно, в будущем потребуется реализовать несколько разных алгоритмов для вычисления почасовой оплаты — скажем, OvertimeHourlyPayCalculator и Straight- TimeHourlyPayCalculator . В этом случае данная функция не может быть статиче- ской . Ее следует оформить как нестатическую функцию Employee 334 Разное 335 В общем случае отдавайте предпочтение нестатическим методам перед статиче- скими . Если сомневаетесь, сделайте функцию нестатической . Если вы твердо уве- рены, что функция должна быть статической, удостоверьтесь в том, что от нее не потребуется полиморфное поведение . g19: Используйте пояснительные переменные Кент Бек писал об этом в своей великой книге «Smalltalk Best Practice Patterns» [Beck97, p . 108], а затем позднее — в столь же великой книге «Implementation Patterns» [Beck07] . Один из самых эффективных способов улучшения удобо- читаемости программы заключается в том, чтобы разбить обработку данных на промежуточные значения, хранящиеся в переменных с содержательными именами . Возьмем следующий пример из FitNesse: Matcher match = headerPattern.matcher(line); if(match.find()) { String key = match.group(1); String value = match.group(2); headers.put(key.toLowerCase(), value); } Простое использование пояснительных переменных четко объясняет, что первое совпадение содержит ключ ( key ), а второе — значение ( value ) . Перестараться в применении пояснительных переменных трудно . Как правило, чем больше пояснительных переменных, тем лучше . Поразительно, насколько очевидным иногда становится самый невразумительный модуль от простого разбиения обработки данных на промежуточные значения с удачно выбранными именами . g20: Имена функций должны описывать выполняемую операцию Взгляните на следующий код: Date newDate = date.add(5); Как вы думаете, что он делает — прибавляет пять дней к date ? А может, пять не- дель или часов? Изменяется ли экземпляр date , или функция возвращает новое значение Date без изменения старого? По вызову невозможно понять, что делает эта функция . Если функция прибавляет пять дней с изменением date , то она должна назы- ваться addDaysTo или increaseByDays . С другой стороны, если функция возвращает новую дату, смещенную на пять дней, но не изменяет исходного экземпляра date , то она должна называться daysLater или daysSince 335 336 Глава 17 . Запахи и эвристические правила Если вам приходится обращаться к реализации (или документации), чтобы понять, что делает та или иная функция, постарайтесь найти более удачное имя или разбейте функциональность на меньшие функции с более понятными име- нами . g21: Понимание алгоритма Очень много странного кода пишется из-за того, что люди не утруждают себя пониманием алгоритмов . Они заставляют программу работать «грубой силой», набивая ее командами if и флагами, вместо того чтобы остановиться и подумать, что же в действительности происходит . Программирование часто сопряжено с исследованиями . Вы думаете, что знаете подходящий алгоритм для решения задачи, но потом вам приходится возиться с ним, подправлять и затыкать щели, пока вы не заставите его «работать» . А как вы определили, что он «работает»? Потому что алгоритм прошел все тесты, ко- торые вы смогли придумать . В этом подходе нет ничего плохого . Более того, часто только так удается заста- вить функцию делать то, что она должна делать (по вашему мнению) . Однако ограничиться «работой» в кавычках недостаточно . Прежде чем откладывать в сторону готовую функцию, убедитесь в том, что вы понимаете, как она работает . Прохождения всех тестов недостаточно . Вы должны знать 1 , что ваше решение правильно . Один из лучших способов достичь этого знания и понимания — разбить функцию на фрагменты настолько чистые и выразительные, что вам станет совершенно очевидно, как работает данная функция . g22: Преобразование логических зависимостей в физические Если один модуль зависит от другого, зависимость должна быть не только логи- ческой, но и физической . Зависимый модуль не должен делать никаких предпо- ложений (иначе говоря, создавать логические зависимости) относительно того модуля, от которого он зависит . Вместо этого он должен явно запросить у этого модуля всю необходимую информацию . Допустим, вы пишете функцию, которая выводит текстовый отчет об от ра- ботанном времени . Класс с именем HourlyReporter собирает все данные в удоб- ной форме и передает их классу HourlyReportFormatter для вывода (лис тинг 17 .1) . 1 Знать, как работает ваш код, и знать, делает ли алгоритм то, что требуется, — не одно и то же . Не уверены в правильности выбора алгоритма? Нередко это суровая правда жизни . Но если вы не уверены в том, что делает ваш код, то это обычная лень . 336 Разное 337 листинг 17 .1 . HourlyReporter.java public class HourlyReporter { private HourlyReportFormatter formatter; private List private final int PAGE_SIZE = 55; public HourlyReporter(HourlyReportFormatter formatter) { this.formatter = formatter; page = new ArrayList } public void generateReport(List |