Главная страница

Создание, анализ ирефакторинг


Скачать 3.16 Mb.
НазваниеСоздание, анализ ирефакторинг
Дата29.09.2022
Размер3.16 Mb.
Формат файлаpdf
Имя файлаChistyj_kod_-_Sozdanie_analiz_i_refaktoring_(2013).pdf
ТипКнига
#706087
страница36 из 49
1   ...   32   33   34   35   36   37   38   39   ...   49
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 page;
private final int PAGE_SIZE = 55;
public HourlyReporter(HourlyReportFormatter formatter) {
this.formatter = formatter;
page = new ArrayList();
}
public void generateReport(List
1   ...   32   33   34   35   36   37   38   39   ...   49


написать администратору сайта