Это 17-я глава Запахи и эвристические правила книги «Чистый код» Роберта Мартина

В своей замечательной книге «Refactoring» Мартин Фаулер описывает много различных «запахов кода». Следующий список содержит много «запахов», предложенных Мартином, а также ряд моих собственных дополнений. Кроме того, в него были включены некоторые приемы и эвристические правила, которые я часто применяю в своей работе. Чтобы построить этот список, я просмотрел и переработал несколько разных программ. При внесении каждого изменения я спрашивал себя, почему я это делаю, и записывал результат. Так появился довольно длинный список того, что, на мой взгляд, «дурно пахнет» при чтении кода. Предполагается, что вы будете читать список от начала к концу, а также использовать его как краткий справочник.

Комментарии

С1: Неуместная информация

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

С2: Устаревший комментарий

Комментарий, содержимое которого потеряло актуальность, считается устаревшим. Комментарии стареют довольно быстро. Не пишите комментарии, которые с течением времени устареют. Обнаружив устаревший комментарий, обновите его или избавьтесь от него как можно быстрее. Устаревшие комментарии часто «отрываются» от кода, который они когда-то описывали. Так в вашем коде появляются плавучие островки недостоверности и бесполезности.

СЗ: Избыточный комментарий

Избыточным считается комментарий, описывающий то, что и так очевидно. Например:

i++; // Увеличение переменной i

Или другой пример — комментарий Javadoc, который содержит не больше (а вернее, меньше) полезной информации, чем простая сигнатура функции:

/**
* @param sellRequest
* @return
* @throws ManagedComponentException
*/
public SellResponse beginSellItem(SellRequest sellRequest) throws ManagedComponentException

Комментарии должны говорить то, что не может сказать сам код.

С4: Плохо написанный комментарий

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

С5: Закомментированный код

Фрагменты закомментированного кода выводят меня из себя. Кто знает, когда был написан этот код? Кто знает, есть от него какая-нибудь польза или нет? Однако никто не удаляет закомментированный код — все считают, что он понадобится кому-то другому.

Этот код только попусту занимает место, «загнивая» и утрачивая актуальность с каждым днем. В нем вызываются несуществующие функции. В нем используются переменные, имена которых давно изменились. В нем соблюдаются устаревшие конвенции. Он загрязняет модуль, в котором он содержится, и отвлекает людей, которые пытаются его читать. Закомментированный код отвратителен. Увидев закомментированный код, удалите его. Не беспокойтесь, система управления исходным кодом его не забудет. Если кому-то этот код действительно понадобится, то он сможет вернуться к предыдущей версии. Не позволяйте закомментированному коду портить вам жизнь.

Рабочая среда

Е1: Построение состоит из нескольких этапов

Построение проекта должно быть одной тривиальной операцией. Без выборки многочисленных фрагментов из системы управления исходным кодом. Без длинных серий невразумительных команд или контекстно-зависимых сценариев для построения отдельных элементов. Без поиска дополнительных файлов в формате JAR, XML и других артефактов, необходимых для вашей системы. Сначала вы проверяете систему одной простой командой, а потом вводите другую простую команду для ее построения.

svn get mySystem
cd mySystem
ant all

E2: Тестирование состоит из нескольких этапов

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

Функции

F1: Слишком много аргументов

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

F2: Выходные аргументы

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

F3: Флаги в аргументах

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

F4: Мертвые функции

Если метод ни разу не вызывается в программе, то его следует удалить. Хранить «мертвый код» расточительно. Не бойтесь удалять мертвые функции. Не забудьте, что система управления исходным кодом позволит восстановить их в случае необходимости.

Разное

G1: Несколько языков в одном исходном файле

Современные среды программирования позволяют объединять в одном исходном файле код, написанный на разных языках. Например, исходный файл на языке Java может содержать вставки XML, HTML, YAML, JavaDoc, English, JavaScript и т. д. Или, скажем, наряду с кодом HTML в файле JSP может присутствовать код Java, синтаксис библиотеки тегов, комментарии на английском языке, комментарии Javadoc, XML, JavaScript и т. д. В лучшем случае результат получается запутанным, а в худшем — неаккуратным и ненадежным. В идеале исходный файл должен содержать код на одном — и только одном! — языке. На практике без смешения языков обойтись, скорее всего, не удастся. Но по крайней мере следует свести к минимуму как количество, так и объем кода на дополнительных языках в исходных файлах.

G2: Очевидное поведение не реализовано

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

Day day = DayDate.StringToDay(String dayNarne);

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

G3: Некорректное граничное поведение

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

G4: Отключенные средства безопасности

Авария на Чернобыльской станции произошла из-за того, что директор завода отключил все механизмы безопасности, один за другим. Они усложняли проведение эксперимента. Результат — эксперимент так и не состоялся, а мир столкнулся с первой серьезной катастрофой в гражданской атомной энергетике. Отключать средства безопасности рискованно. Ручное управление serialVersion-UID бывает необходимо, но оно всегда сопряжено с риском. Иногда отключение некоторых (или всех!) предупреждений компилятора позволяет успешно построить программу, но при этом вы рискуете бесконечными отладочными сеансами. Не отключайте сбойные тесты, обещая себе, что вы заставите их проходить позднее, — это так же неразумно, как считать кредитную карту источником бесплатных денег.

G5: Дублирование

Это одно из самых важных правил в книге и к нему следует относиться очень серьезно. Практически каждый автор, пишущий о проектировании программного обеспечения, упоминает это правило. Дэйв Томас (Dave Thomas) и Энди Хант (Andy Hunt) назвали его принципом DRY («Don’t Repeat Yourself», то есть «неповторяйтесь») [PRAG]. Кент Бек сделал его одним из основных принципов экстремального программирования в формулировке «Один, и только один раз». Рон Джеффрис (Ron Jeffries) ставит это правило на второе место, после требования о прохождении всех тестов. Каждый раз, когда в программе встречается повторяющийся код, он указывает на упущенную возможность для абстракции. Возможно, дубликат мог бы стать функцией или даже отдельным классом. «Сворачивая» дублирование в подобные абстракции, вы расширяете лексикон языка программирования. Другие программисты могут воспользоваться созданными вами абстрактными концепциями. Повышение уровня абстракции ускоряет программирование и снижает вероятность ошибок. Простейшая форма дублирования — куски одинакового кода. Программа выглядит так, словно у программиста дрожат руки, и он снова и снова вставляет один и тот же фрагмент. Такие дубликаты заменяются простыми методами. Менее тривиальная форма дублирования — цепочки switch/case или i f/el se, снова и снова встречающиеся в разных модулях и всегда проверяющие одинаковые наборы условий. Вместо них надлежит применять полиморфизм. Еще сложнее модули со сходными алгоритмами, но содержащие похожих строк кода. Однако дублирование присутствует и в этом случае. Проблема решается применением паттернов ШАБЛОННЫЙ МЕТОД или СТРАТЕГИЯ [GOF]. В сущности, большинство паттернов проектирования, появившихся за последние 15 лет, представляет собой хорошо известные способы борьбы с дублированием. Нормальные формы Кодда устраняют дублирование в схемах баз данных. Само объектно-ориентированное программирование может рассматриваться как стратегия модульной организации кода и устранения дубликатов. Естественно, это относится и к структурному программированию. Надеюсь, я достаточно четко выразил свою мысль. Ищите и устраняйте дубликаты повсюду, где это возможно.

G6: Код на неверном уровне абстракции

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

Рассмотрим следующий фрагмент:

public interface Stack {
   Object pop() throws EmptyException;
   void push(Object o) throws EullException;
   double percentFull();

   class EmptyException extends Exception {}
   class Full Exception extends Exception {}
}

Функция percentFull находится на неверном уровне абстракции. Существует много реализаций стека, в которых концепция заполнения выглядит разумно, однако другие реализации могут не знать, до какой степени заполнен стек. Следовательно, эта функция должна располагаться в производном интерфейсе — например, BoundedStack. Возможно, вы думаете, что для неограниченного стека реализация может просто вернуть 0? Проблема в том, что абсолютно неограниченного стека не существует. Вам не удастся предотвратить исключение OutOfMemoryException, проверив условие stack.percentFull() < 50.0 Если ваша реализация функции возвращает 0, то она попросту врет. Суть в том, что ложь и фикции не способны компенсировать неверного размещения абстракций. Разделение абстракций — одна из самых сложных задач, решаемых разработчиками. Если выбор сделан неверно, не надейтесь, что вам удастся найти простое обходное решение.

G7: Базовые классы, зависящие от производных

Самая распространенная причина для разбиения концепций на базовые и производные классы состоит в том, чтобы концепции базового класса, относящиеся к более высокому уровню, были независимы от низкоуровневых концепций производных классов. Следовательно, когда в базовом классе встречаются упоминания имен производных классов, значит, в проектировании что-то сделано не так. В общем случае базовые классы не должны ничего знать о своих производных классах. Конечно, у этого правила имеются свои исключения. Иногда количество производных классов жестко фиксировано, а в базовом классе присутствует код для выбора между производными классами. Подобная ситуация часто встречается в реализациях конечных автоматов. Однако в этом случае между базовым и производными классами существует жесткая привязка, и они всегда размещаются вместе в одном файле jar. В общем случае нам хотелось бы иметь возможность размещения производных и базовых классов в разных файлах. Размещение производных и базовых классов в разных файлах jar, при котором базовые файлы jar ничего не знают о содержимом производных файлов jar, позволяет организовать развертывание систем в формате дискретных, независимых компонентов. Если в такие компоненты будут внесены изменения, то они развертываются заново без необходимости повторного развертывания базовых компонентов. Такая архитектура значительно сокращает последствия от вносимых изменений и упрощает сопровождение систем в условиях реальной эксплуатации.

G8: Слишком много информации

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

G9: Мертвый код

Мертвым кодом называется код, не выполняемый в ходе работы программы. Он содержится в теле команды i f, проверяющей невозможное условие. Он содержится в секции catch для блока try, никогда не инициирующего исключения. Он содержится в маленьких вспомогательных методах, которые никогда не вызываются, или в никогда не встречающихся условиях switch/case. Мертвый код плох тем, что спустя некоторое время он начинает «плохо пахнуть». Чем древнее код, тем сильнее и резче запах. Дело в том, что мертвый код не обновляется при изменении архитектуры. Он компилируется, но не соответствует более новым конвенциям и правилам. Он был написан в то время, когда система была другой. Обнаружив мертвый код, сделайте то, что положено делать в таких случаях: достойно похороните его. Удалите его из системы.

G10: Вертикальное разделение

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

G11: Непоследовательность

Если некая операция выполняется определенным образом, то и все похожие операции должны выполняться так же. Это правило возвращает нас к «принципу наименьшего удивления». Ответственно подходите к выбору новых схем и обозначений, а если уж выбрали — продолжайте следовать им. Если в функцию включена переменная response для хранения данных HttpServletResponse, будьте последовательны и используйте такое же имя переменной в других функциях, работающих с объектами HttpServletResponse. Если метод называется processVerificationRequest, присваивайте похожие имена (например, processDeletionRequest) методам, обрабатывающим другие запросы.Последовательное соблюдение подобных схем и правил существенно упрощает чтение и модификацию кода.

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);
   }
}

Метод 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.%ld\n”,
      employee.getName(),
      employee.getTenthsWorked()/10,
      employee.getTenthsWorked()%10);
   }
}

Очевидно, метод reportHours завидует классу HourlyEmployee. С другой стороны, мы не хотим, чтобы класс HourlyEmployee знал о формате отчета. Перемещение форматной строки в класс HourlyEmployee нарушает некоторые принципы объектно-ориентированного проектирования. Такое размещение привязывает 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);
   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 straightPayO + 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));
}

Такая запись выглядит компактной и плотной, но разбираться в ней — сущее мучение. Не жалейте времени на то, чтобы сделать намерения своего кода максимально прозрачными для читателей.

G17: Неверное размещение

Одно из самых важных решений, принимаемых разработчиком, — выбор места для размещения кода. Например, где следует объявить константу PI? В классе Math? А может, ей место в классе Trigonometry? Или в классе Circle? В игру вступает принцип наименьшего удивления. Код следует размещать там, где читатель ожидает его увидеть. Константа PI должна находиться там, где объявляются тригонометрические функции. Константа OVERTIMERATE объявляется в классе HourlyPayCalculator. Иногда мы пытаемся «творчески» подойти к размещению функциональности. Мы размещаем ее в месте, удобном для нас, но это не всегда выглядит естественно для читателя кода. Предположим, потребовалось напечатать отчет с общим количеством отработанных часов. Мы можем просуммировать часы в коде, печатающем отчет, или же накапливать сумму в коде обработки учетных карточек рабочего времени. Чтобы принять решение, можно посмотреть на имена функций. Допустим, в модуле отчетов присутствует функция с именем getTotalHours, а в модуле обработки учетных карточек присутствует функция saveTimeCard. Какая из этих двух функций, если судить по имени, наводит на мысль о вычислении суммы? Ответ очевиден. Очевидно, по соображениям производительности сумму правильнее вычислять при обработке карточек, а не при печати отчета. Все верно, но этот факт должен быть отражен в именах функций. Например, в модуле обработки учетных карточек должна присутствовать функция computeRunningTotalOfHours. === G18: Неуместные статические методы

Math.max(double a, double b) — хороший статический метод. Он работает не с одним экземпляром; в самом деле, запись вида new Math().max(a ,b) или даже a.max(b) выглядела бы довольно глупо. Все данные, используемые шах, берутся из двух аргументов, а не из некоего объекта-«владельца». А главное, что метод Math.max почти наверняка не потребуется делать полиморфным. Но иногда мы пишем статические функции, которые статическими быть не должны.

Пример:

HourlyPayCalculator.calculatePay(employee, overtimeRate)

Эта статическая функция тоже выглядит вполне разумно. Она не работает ни с каким конкретным объектом и получает все данные из своих аргументов. Однако нельзя исключать, что эту функцию потребуется сделать полиморфной. Возможно, в будущем потребуется реализовать несколько разных алгоритмов для вычисления почасовой оплаты — скажем, OvertimeHourlyPayCalculator и StraightTimeHourlyPayCalculator. В этом случае данная функция не может быть статической. Ее следует оформить как нестатическую функцию Employee. В общем случае отдавайте предпочтение нестатическим методам перед статическими. Если сомневаетесь, сделайте функцию нестатической. Если вы твердо уверены, что функция должна быть статической, удостоверьтесь в том, что от нее не потребуется полиморфное поведение.

G19: Используйте пояснительные переменные

Кент Бек писал об этом в своей великой книге «Smalltalk Best Practice Patterns» [Beck97, p. 108], а затем позднее — в столь же великой книге «Implementation Patterns» [Beck07]. Один из самых эффективных способов улучшения удобочитаемости программы заключается в том, чтобы разбить обработку данных на промежуточные значения, хранящиеся в переменных с содержательными именами. Возьмем следующий пример из FitNesse:

Matcher match = headerPattern.matcher(line);

if (match.find())
{
   String key = match.group(l);
   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.

Если вам приходится обращаться к реализации (или документации), чтобы понять, что делает та или иная функция, постарайтесь найти более удачное имя или разбейте функциональность на меньшие функции с более понятными именами.

G21: Понимание алгоритма

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

G22: Преобразование логических зависимостей в физические

Если один модуль зависит от другого, зависимость должна быть не только логической, но и физической. Зависимый модуль не должен делать никаких предположений (иначе говоря, создавать логические зависимости) относительно того модуля, от которого он зависит. Вместо этого он должен явно запросить у этого модуля всю необходимую информацию. Допустим, вы пишете функцию, которая выводит текстовый отчет об отработанном времени. Класс с именем HourlyReporter собирает все данные в удобной форме и передает их классу HourlyReportFormatter для вывода

public class HourlyReporter {
   private HourlyReportFormatter formatter;
   private List<LineItem> page;
   private final int PAGE_SIZE = 55;

   public HourlyReporter(HourlyReportFormatter formatter) {
      this.formatter = formatter;

      page = new ArrayList<LineItem>();
   }

   public void generateReport(List<HourlyEmployee> employees) {

      for (HourlyEmployee e ; employees) {
         addLineltemToPage(e);

         if (page.sizeO == PAGE_SIZE) printAndClearItemList();
      }

      if (page.size() > 0) printAndClearltemList();
   }

   private void printAndClearltemList() {
      formatter.format(page);
      page.clear();
   }

   private void addLineItemToPage(HourlyEmployee e) {
      Lineltem item = new Lineltem();
      item, name = e.getName();
      item.hours = e.getTenthsWorked() / 10;
      item.tenths = e.getTenthsWorked() % 10;
      page.add(itern);
   }

   public class Lineltem {
      public String name;
      public int hours;
      public int tenths;
   }
}

У этого кода имеется логическая зависимость, лишенная физического воплощения. Удастся ли вам найти ее? Речь идет о константе PAGE_SIZE. Почему класс HourlyReporter должен знать размер страницы? За размер страницы должен отвечать класс HourlyReportFormatter. Факт объявления PAGESIZE в HourlyReporter указывает на неверное размещение [G17]. Разработчик полагает, что классу HourlyReporter необходимо знать размер страницы. Такое предположение является логической зависимостью. Работа HourlyReporter зависит от того факта, что HourlyReportFormatter поддерживает размер страницы до 55. Если какая-то реализация HourlyReportFormatter не сможет работать с такими страницами, произойдет ошибка. Чтобы создать физическое представление этой зависимости, мы можем включить в HourlyReportFormatter новый метод с именем getMaxPageSize(). В дальнейшем HourlyReporter вызывает эту функцию вместо того, чтобы использовать константу PAGEJIZE.

G23: Используйте полиморфизм вместо if/Else или switch/Case

Эта рекомендация может показаться странной, если вспомнить главу 6. Ведь в этой главе говорилось, что в тех частях системы, в которые с большей вероятностью будут добавляться новые функции, а не новые типы, команды switch вполне уместны. Во-первых, команды switch чаще всего используются только потому, что они представляют очевидное решение методом «грубой силы», а не самое уместное решение для конкретной ситуации. Таким образом, это эвристическое правило напоминает нам о том, что до применения switch следует рассмотреть возможность применения полиморфизма. Во-вторых, ситуации, в которых состав функций менее стабилен, чем состав типов, встречаются относительно редко. Следовательно, к каждой конструкции switch следует относиться с подозрением.

Я использую правило «ОДНОЙ КОМАНДЫ SWITCH»: для каждого типа выбора программа не должна содержать более одной команды switch. Множественные конструкции switch следует заменять полиморфными объектами.

G24: Соблюдайте стандартные конвенции

Все рабочие группы должны соблюдать единые стандарты кодирования, основанные на отраслевых нормах. Стандарт кодирования определяет, где объявляются переменные экземпляров; как присваиваются имена классов, методов и переменных; где размещаются фигурные скобки и т. д. Документ с явным описанием этих правил не нужен — сам код служит примером оформления. Правила должны соблюдаться всеми участниками группы. Это означает, что каждый участник группы должен быть достаточно разумным, чтобы понимать: неважно, как именно размещаются фигурные скобки, если только все согласились размещать их одинаковым образом.

G25: Заменяйте «волшебные числа» именованными константами

Вероятно, это одно из самых древних правил разработки. Помню, оно встречалось мне еще в 60-х годах, в учебниках COBOL, FORTRAN и PL/1 для начинающих. В общем случае присутствие «сырых» чисел в коде нежелательно. Числа следует скрыть в константах с содержательными именами. Например, число 86,400 следует скрыть в константе SECONDSPERDAY. Если в странице отчета выводится 55 строк, число 55 следует скрыть в константе LINES_PER_PAGE. Некоторые числа так легко узнаются, что их не обязательно скрывать за именованными константами — при условии, что они используются в сочетании с предельно ясным кодом.

Пример:

double milesWalked = feetWalked/5280.0;
int dailyPay = hourlyRate * 8;
double circumference = radius * Math.PI * 2;

Нужны ли константы FEET_PER_MILE, W0RK_H0URS_PER_DAY и TWO в этих примерах?

Разумеется, последний случай выглядит особенно абсурдно. В некоторых формулах константы попросту лучше воспринимаются в числовой записи. По поводу WORKHOURSPERDAY можно спорить, потому что законы и нормативы могут изменяться. С другой стороны, формула с числом 8 читается настолько удобно, что мне просто не хочется нагружать читателя кода лишними 17 символами. А число 5280 — количество футов в миле — настолько хорошо известно и уникально, что читатель сразу узнает его, даже если оно будет располагаться вне какого-либо контекста. Такие константы, как 3.141592653589793, тоже хорошо известны и легко узнаваемы. Однако вероятность ошибки слишком велика, чтобы оставлять их в числовой форме. Встречая значение 3.1415927535890793, вы сразу догадываетесь, что перед вами число п, и не проверяете его (а вы заметили ошибку в одной цифре?). Также мы не хотим, чтобы в программах использовались сокращения 3.14,3.14159,3.142 и т. д. К счастью, значение Math. PI уже определено за нас. Термин «волшебное число» относится не только к числам. Он распространяется на все лексемы, значения которых не являются самодокументирующими.

Пример:

assertEquals(7777, Employee.find("John Doe").employeeNumber());

В этом проверочном условии задействованы два «волшебных числа». Очевидно, первое — 7777, хотя его смысл далеко не так очевиден. Второе «волшебное число» — строка "John Doe". Ее смысл тоже выглядит весьма загадочно. Оказывается, "John Doe" — имя работника с табельным номером 7777 в тестовой базе данных, созданной нашей группой. Все участники группы знают, как подключаться к этой базе данных. В базе уже хранятся тестовые записи с заранее известными значениями и атрибутами. Также выясняется, что "John Doe" — единственный работник с почасовой оплатой в тестовой базе данных. Следовательно, эта проверка должна выглядеть так:

assertEquals(HOURLY_EMPLOYEE_ID, Employee.find(HOURLY_EMPLOYEE_NAME).employeeNumber());

G26: Будьте точны

Наивно ожидать, что первая запись, возвращаемая по запросу, является единственной. Использовать числа с плавающей точкой для представления денежных сумм — почти преступление. Отсутствие блокировок и/или управления транзакциями только потому, что вы думаете, что одновременное обновление маловероятно — в лучшем случае халатность. Объявление переменной с типом ArrayList там, где более уместен тип List — чрезмерное ограничение. Объявление всех переменных защищенными по умолчанию — недостаточное ограничение. Принимая решение в своем коде, убедитесь в том, что вы действуете предельно точно и аккуратно. Знайте, почему принимается решение, и как вы собираетесь поступать с исключениями из правила. Не ленитесь обеспечивать точность своих решений. Если вы решили вызвать функцию, которая может вернуть null — проверьте возвращаемое значение. Если вы запрашиваете из базы данных запись, которая, по вашему мнению, является единственной — проверьте, не вернул ли запрос дополнительные записи. Если вам нужно работать с денежными суммами, используйте целые числа и округляйте результат по действующим правилам. Если в программе существует возможность одновременного объявления, реализуйте ту или иную разновидность блокировки. Неоднозначности и неточности в коде объясняются либо недопониманием, либо ленью. В любом случае от них следует избавиться.

G27: Структура важнее конвенций

Воплощайте архитектурные решения на уровне структуры кода; она важнее стандартов и конвенций. Содержательные имена полезны, но структура, заставляющая пользователя соблюдать установленные правила, важнее. Например, конструкции switch/case с хорошо выбранными именами элементов перечисления уступают базовым классам с абстрактными методами. Ничто не вынуждает пользователя применять одинаковую реализацию switch/case во всех случаях; с другой стороны, базовые классы заставляют его реализовать все абстрактные методы в конкретных классах.

G28: Инкапсулируйте условные конструкции

В булевской логике достаточно трудно разобраться и вне контекста команд if или while. Выделите в программе функции, объясняющие намерения условной конструкции. Например, команда

if (shouldBeDeleted(timer))

выразительнее команды

if (timer.hasExpired() && !timer.isRecurrent())

G29: Избегайте отрицательных условий

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

if (buffer.shouldCompact())

предпочтительнее записи

if (!buffer.shouldNotCompact())

G30: Функции должны выполнять одну операцию

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

Пример:

public void pay() {
   for (Employee e : employees) {
      if (e.isPayday()) {
         Money pay = e.calculatePay();
         e.deliverPay(pay);
      }
   }
}

Эта функция выполняет сразу три операции: она перебирает всех работников; проверяет, начислены ли работнику какие-то выплаты; и наконец, производит оплату. Код лучше записать в следующем виде:

public void pay() {
   for (Employee e : employees)
      paylfNecessary(e);
}

private void payIfNecessary(Employee e) {
   if (e.isPayday())
      calculateAndDeliverPay(e);
}

private void calculateAndDeliverPayCEmployee e) {
   Money pay = e.calculatePay();
   e.deliverPay(pay);
}

Каждая из этих функций выполняет только одну операцию.

G31: Скрытые временные привязки

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

Рассмотрим следующий пример:

public class MoogDiver {
   Gradient gradient;
   List<Spline> splines;

   public void dive(String reason) {
      saturateGradient();
      reticulateSplines();
      diveForMoog(reason);
   }
}

Порядок вызова трех функций важен. Сначала вызывается saturateGradient(), затем reticulateSplines() и только после этого diveForMoog(). К сожалению, код не обеспечивает принудительного соблюдения временной привязки. Ничто не мешает другому программисту вызвать reticulateSplines до saturateGradient, и все кончится исключением UnsaturatedGradientException.

Более правильное решение выглядит так:

public class MoogDiver {
   Gradient gradient;
   List<Spline> splines;

   public void dive(String reason) {
      Gradient gradient = saturateGradient();
      List<Spline> splines = reticulateSplines(gradient);
      diveForMoog(splines, reason);
   }
}

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

G32: Структура кода должна быть обоснована

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

Например, недавно я занимался объединением изменений в FitNesse и обнаружил, что один из наших авторов использовал следующую запись:

public class AliasLinkWidget extends ParentWidget
{
   public static class VariableExpandingWidgetRoot {
   ...
   ...
}

Проблема в том, что VariableExpandingWidgetRoot незачем находиться в облети видимости AliasLinkWidget. Более того, класс AliasLinkWidget.VariableExpandingWidgetRoot использовался посторонними классами, которые не имели никакого отношения к AliasLinkWidget. Возможно, программист разместил VariableExpandingWidgetRoot в AliasWidget по соображениям удобства, а может, он действительно полагал, что область видимости этого класса должна находиться внутри области видимости AliasWidget. Какими бы причинами он ни руководствовался, результат выглядит необоснованным. Открытые классы, не являющиеся вспомогательными по отношению к другому классу (то есть используемыми только в его внутренних операциях), не должны размещаться внутри других классов. По стандартным правилам такие классы объявляются на верхнем уровне своих пакетов.

G33: Инкапсулируйте граничные условия

Отслеживать граничные условия нелегко. Разместите их обработку в одном месте. Не позволяйте им «растекаться» по всему коду. Не допускайте, чтобы в вашей программе кишели многочисленные +1 и -1. Возьмем простой пример из FIT:

if (level + 1 < tags.length)
{
   parts = new Parse(body, tags, level + 1. offset + endTag);
   body - null;
}

Обратите внимание: level+1 здесь встречается дважды. Это граничное условие, которое следует инкапсулировать в переменной — например, с именем nextLevel:

int nextLevel - level + 1;
if (nextl_evel < tags.length)
{
   parts - new Parse(body. tags, nextLevel. offset + endTag);
   body - null;
}

G34: Функции должны быть написаны на одном уровне абстракции

Все команды функции должны быть сформулированы на одном уровне абстракции, который расположен одним уровнем ниже операции, описываемой именем функции. Возможно, это эвристическое правило сложнее всего правильно интерпретировать и соблюдать. Идея достаточно тривиальна, но люди слишком хорошо справляются со смешением разных уровней абстракции. Для примера возьмем следующий код из FitNesse:

public String render() throws Exception
{
   StringBuffer html = new StringBuffer("<hr");

   if (size > 0)
      html.append(" size=\"").append(size + l).append("\"");
   html.append(">");

   return html.toString();
}

Разобраться в происходящем несложно. Функция конструирует тег HTML, который рисует на странице горизонтальную линию. Толщина линии задается переменной size. А теперь взгляните еще раз. В этом методе смешиваются минимум два уровня абстракции. Первый уровень — наличие толщины у горизонтальной линии. Второй уровень — синтаксис тега HR. Код позаимствован из модуля HruleWidget проекта FitNesse. Модуль распознает строку из четырех и более дефисов и преобразует ее в соответствующий тег HR. Чем больше дефисов, тем больше толщина. Я переработал этот фрагмент кода так, как показано ниже. Обратите внимание: имя поля size изменено в соответствии с его истинным назначением (в нем хранится количество дополнительных дефисов).

public String render() throws Exception
{
   HtmlTag hr = new HtmlTag("hr");

   if (extraDashes > 0)
      hr.addAttribute("size", hrSize(extraDashes));

   return hr.htm();
}

private String hrSize(int height)
{
   int hrSize = height + 1;
   return String. format(“%d", hrSize);
}

Изменение разделяет два уровня абстракции. Функция render просто конструирует тег HR, ничего не зная о синтаксисе HTML этого тега. Модуль HtmlTag берет на себя все хлопоты с синтаксисом. Более того, при внесении этого изменения я обнаружил неприметную ошибку. Исходный код не закрывал тег HR косой чертой, как того требует стандарт XHTML (иначе говоря, он выдавал <hr> вместо <hr/>), хотя модуль HtmlTag был давно приведен в соответствие со стандартом XHTML. Разделение уровней абстракции — одна из самых важных и одновременно самых сложных в реализации функций рефакторинга. В качестве примера возьмем следующий код — мою первую попытку разделения уровней абстракции в методе HruleWidget.render

public String render() throws Exception
{
   HtmlTag hr = new HtmlTag("hr");

   if (size > 0) {
      hr.addAttribute("size", ""+(size+1));
   }

   return hr.html();
}

На этой стадии я стремился к тому, чтобы создать необходимое разделение, и обеспечить прохождение тестов. Мне удалось легко добиться этой цели, но в созданной функции по-прежнему смешивались разные уровни абстракции — на этот раз конструирование тега HR и интерпретация/форматирование переменной size. Таким образом, при разбиении функции по уровням абстракции иногда обнаруживаются новые уровни, скрытые прежней структурой.

G35: Храните конфигурационные данные на высоких уровнях

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

public static void main(String[] args) throws Exception
{
   Arguments arguments = parseCommandLine(args);
   ...
}

public class Arguments
{
   public static final String DEFAULT_PATH = ".";
   public static final String DEFAULT_R00T = "FitNesseRoot";
   public static final int DEFAULT_PORT = 80;
   public static final int DEFAULT_VERSION_DAYS = 14;
}

Аргументы командной строки разбираются в самой первой исполняемой строке FitNesse. Значения аргументов по умолчанию задаются в начале класса Argument. Читателю не приходится спускаться на нижние уровни системы за командами следующего вида:

   if (arguments.port == 0) // 80 по умолчанию

Конфигурационные константы находятся на очень высоком уровне. Если потребуется, их можно легко изменить. Их значения передаются на более низкие уровни иерархии другим компонентам приложения. Значения этих констант не принадлежат нижним уровням приложения.

G36: Избегайте транзитивных обращений

В общем случае модуль не должен обладать слишком полной информацией о тех компонентах, с которыми он взаимодействует. Точнее, если А взаимодействует с В, а В взаимодействует с С, то модули, использующие А, не должны знать о С (то есть нежелательны конструкции вида а. getB(). getC().doSomething();). Иногда это называется «законом Деметры». Прагматичные программисты используют термин «умеренный код». В любом случае все сводится к тому, что модули должны обладать информацией только о тех модулях, с которыми они непосредственно взаимодействуют, а не располагать навигационной картой всей системы. Если в нескольких модулях используется та или иная форма команды a.getВ().getC(), то в дальнейшем вам будет трудно изменить архитектуру системы, вставив между В и С промежуточный компонент Q. Придется найти каждое вхождение a.getB().getQ() и преобразовать его в a.getB().getQ().getC(). Так образуются жесткие, закостеневшие архитектуры. Слишком многие модули располагают слишком подробной информацией о системе. Весь необходимый сервис должен предоставляться компонентами, с которыми напрямую взаимодействует модуль. Не заставляйте пользователя странствовать по графу объектов системы в поисках нужного метода. Проблема должна решаться простыми вызовами вида

myCollaborator.doSomething()

Имена

N1: Используйте содержательные имена

Не торопитесь с выбором имен. Позаботьтесь о том, чтобы имена были содержательными. Помните, что смысл может изменяться в ходе развития программного продукта; почаще переосмысливайте уместность выбранных вами имен. Не рассматривайте это как дополнительный «фактор комфортности». Имена в программных продуктах на 90% определяют удобочитаемость кода. Не жалейте времени на то, чтобы выбрать их осмысленно, и поддерживайте их актуальность. Имена слишком важны, чтобы относиться к ним легкомысленно. Возьмем следующий код. Что он делает? Когда я представлю вам тот же код с нормально выбранными именами, вы моментально поймете его смысл, но в этом виде он представляет собой мешанину из символов и «волшебных чисел».

public int х() {
   int q = 0;
   int z = 0;

   for (int kk = 0; kk < 10; kk++) {
      if (l[z] == 10){
         q += 10 + (l[z + 1] + l[z + 2]);
         z += 1;
      }else if (l[z] + l[z + 1] == 10)
         q += 10 + l[z + 2];
         z += 2;
      } else {
         q += l[z] + l[z + 1];
         z += 2;
      }
   }

   return q;
}

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

public int score() {
   int score = 0;
   int frame = 0;

   for (int frameNumber = 0; frameNumber < 10; frameNumber++) {
      if (isStrike(frame)) {
         score += 10 + nextTwoBallsForStrike(frame);
         frame += 1;
      } else if (isSpare(frame)) {
         score += 10 + nextBallForSpare(frame);
         frame += 2;
      } else {
         score += twoBallsInFrame(frame);
         frame += 2;
      }
   }

   return score;
}

Сила хорошо выбранных имен заключается в том, что они дополняют структуру кода описаниями. На основании этих описаний у читателя формируются определенные предположения по поводу того, что делают другие функции модуля. Взглянув на приведенный код, вы сможете представить себе примерную реализацию isStrike(). А при чтении метода isStrike() становится очевидно, что он делает «примерно то, что предполагалось»

private boolean isStrike(int frame) {
   return rolls[frame] == 10:
}

N2: Выбирайте имена на подходящем уровне абстракции

Не используйте имена, передающие информацию о реализации. Имена должны отражать уровень абстракции, на котором работает класс или функция. Сделать это непросто — и снова потому, что люди слишком хорошо справляются со смешением разных уровней абстракции. При каждом просмотре кода вам с большой вероятностью попадется переменная, имя которой выбрано на слишком низком уровне. Воспользуйтесь случаем и измените его. Чтобы ваш код хорошо читался, вы должны серьезно относиться к его непрерывному совершенствованию.

Возьмем следующий интерфейс Modem:

public interface Modem {
   boolean dial (String phoneNumber);
   boolean disconnect();
   boolean send(char c);
   char recv();
   String getConnectedPhoneNumber();
}

На первый взгляд все хорошо — имена функций выглядят разумно. В самом деле, во многих приложениях они точно соответствуют выполняемым операциям. А если для установления связи используется не коммутируемое подключение, а какой-то другой механизм? Например, модемы могут связываться на физическом уровне (как кабельные модемы, обеспечивающие доступ к Интернету во многих домах). А может быть, связь устанавливается посредством отправки номера порта коммутатору через интерфейс USB. Разумеется, концепция телефонных номеров в таких случаях относится к неверному уровню абстракции. Более правильная стратегия выбора имен в таких сценариях может выглядеть так:

public interface Modem {
   boolean connect(String connectionLocator);
   boolean disconnect();
   boolean send(char c);
   char recv();
   String getConnectedLocator();
}

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

N3: По возможности используйте стандартную номенклатуру

Имена проще понять, если они основаны на существующих конвенциях или стандартных обозначениях. Например, при использовании паттерна ДЕКОРАТОР можно включить в имена декорирующих классов слово Decorator. Например, имя AutoHangupModemDecorator может быть присвоено классу, который дополняет класс Modem возможностью автоматического разрыва связи в конце сеанса. Паттерны составляют лишь одну разновидность стандартов. Например, в языке Java функции, преобразующие объекты в строковые представления, часто называются toString. Лучше следовать подобным стандартным конвенциям, чем изобретать их заново. Группы часто разрабатывают собственные стандартные системы имен для конкретного проекта. Эрик Эванс (Eric Evans) называет их всеобщим языком проекта. Широко используйте термины этого языка в своем коде. Чем больше вы используете имена, переопределенные специальным смыслом, относящимся к вашему конкретному проекту, тем проще читателю понять, о чем идет речь в вашем коде.

N4: Недвусмысленные имена

Выбирайте имена, которые максимально недвусмысленно передают назначение функции или переменной. Рассмотрим пример из FitNesse:

private String doRename() throws Exception
{
   if(refactorReferences)
      renameReferences();
   renamePage();
   pathToRename.removeNameFromEnd();
   pathToRename.addNameToEnd(newName);

   return PathParser.render(pathToRename);
}

Имя функции получилось слишком общим и расплывчатым; оно ничего не говорит о том, что делает функция. Ситуацию усугубляет тот факт, что в функции с именем doRename находится функция renamePage! Что можно сказать о различиях между этими функциями по их именам? Ничего. Функцию было бы правильнее назвать renamePageAndOptionallyAllReferences. На первый взгляд имя кажется слишком длинным, но функция вызывается только из одной точки модуля, поэтому ее документирующая ценность перевешивает длину.

N5: Используйте длинные имена для длинных областей видимости

Длина имени должна соответствовать длине его области видимости. Переменным с крошечной областью видимости можно присваивать очень короткие имена, но у переменных с большей областью видимости имена должны быть длинными. Если область видимости переменной составляет всего пять строк, то переменной можно присвоить имя i или j. Возьмем следующий фрагмент из старой стандартной игры «Bowling»:

private void rollMany(int n, int pins)
{
   for (int i=0; i<n; i++)
      g.roll(pins);
}

Смысл переменной i абсолютно очевиден. Какое-нибудь раздражающее имя вида rollCount только затемнило бы смысл этого тривиального кода. С другой стороны, смысл коротких имен переменных и функций рассеивается на длинных дистанциях. Таким образом, чем длиннее область видимости имени, тем более длинным и точным должно быть ее имя.

N6: Избегайте кодирования

Информация о типе или области видимости не должна кодироваться в именах. Префиксы вида т_ или f бессмысленны в современных средах. Кроме того, информация о проекте и/или подсистеме (например, префикс vis_ для подсистемы визуализации) также отвлекает читателя и является избыточной. Современные среды разработки позволяют получить всю необходимую информацию без уродования имен. Поддерживайте чистоту в своих именах, не загрязняйте их венгерской записью.

N7: Имена должны описывать побочные эффекты

Имена должны описывать все, что делает функция, переменная или класс. Не скрывайте побочные эффекты за именами. Не используйте простые глаголы для описания функции, которая делает что-то помимо этой простой операции. Для примера возьмем следующий код из TestNG:

public ObjectOutputStream getOos() throws IOException {
   if (m_oos == null) {
      m_oos = new ObjectOutputStream(m_socket.getOutputStream());
   }

   return m_oos;
}

Функция не ограничивается простым получением moos; она создает объект m_oos, если он не был создан ранее. Таким образом, эту функцию было бы правильнее назвать createOrReturnOos.

Тесты

Т1: Нехватка тестов

Сколько тестов должен включать тестовый пакет? К сожалению, многие программисты руководствуются принципом «Пожалуй, этого хватит». Тестовый пакет должен тестировать все, что может сломаться. Если в системе остались условия, не проверенные тестами, или вычисления, правильность которых не подтверждена, значит, количество тестов недостаточно.

Т2: Используйте средства анализа покрытия кода

Средства анализа покрытия сообщают о пробелах в вашей стратегии тестирования. Они упрощают поиск модулей, классов и функций с недостаточно полным тестированием. Во многих IDE используются визуальные обозначения: строки, покрытые тестами, выделяются зеленым цветом, а непокрытые — красным. Это позволяет легко и быстро обнаружить команды if или catch, тело которых не проверяется тестами.

ТЗ: Не пропускайте тривиальные тесты

Тривиальные тесты пишутся легко, а их информативная ценность превышает затраты времени на их создание.

Т4: Отключенный тест как вопрос

Иногда мы не уверены в подробностях поведения системы, потому что неясны сами требования к программе. Вопрос о требованиях можно выразить в виде теста — закомментированного или помеченного аннотацией @Ignore. Выбор зависит от того, компилируется или нет код, к которому относится неопределенность.

Т5: Тестируйте граничные условия

Особенно тщательно тестируйте граничные условия. Программисты часто правильно реализуют основную часть алгоритма, забывая о граничных ситуациях.

Т6: Тщательно тестируйте код рядом с ошибками

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

Т7: Закономерности сбоев часто несут полезную информацию

Иногда анализ закономерностей в сбоях тестовых сценариев помогает выявить причины возникших проблем. Это еще один аргумент в пользу максимальной полноты тестовых сценариев. Всесторонние наборы тестовых сценариев, упорядоченные логичным образом, выявляют закономерности. Простой пример: вы заметили, что все тесты с входными данными, длина которых превышает пять символов, завершаются неудачей? Или что любой тест, который передает во втором аргументе функции отрицательное число, не проходит? Иногда простая закономерность чередования красного и зеленого в тестовом отчете заставляет нас воскликнуть «Ага!» на пути к правильному решению.

Т8: Закономерности покрытия кода часто несут полезную информацию

Анализ того, какой код выполняется или не выполняется в ходе тестирования, иногда подсказывает причины возможных сбоев в ходе тестирования.

Т9: Тесты должны работать быстро

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

Заключение

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