Чистый код. Создание, анализ и рефакторинг — страница 4 из 94

Хорошо написать код недостаточно. Необходимо поддерживать чистоту кода с течением времени. Все мы видели, как код загнивает и деградирует с течением времени. Значит, мы должны активно поработать над тем, чтобы этого не произошло.

У бойскаутов существует простое правило, которое применимо и к нашей профессии:

Оставь место стоянки чище, чем оно было до твоего прихода[3].

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

Представляете себе работу над проектом, код которого улучшается с течением времени? Но может ли профессионал позволить себе нечто иное? Разве постоянное совершенствование не является неотъемлемой частью профессионализма?

Предыстория и принципы

Эта книга во многих отношениях является «предысторией» для книги, написанной мной в 2002 году: «Agile Software Development: Principles, Patterns, and Practices» (сокращенно PPP). Книга PPP посвящена принципам объектно-ориентированного проектирования и практическим приемам, используемым профессиональными разработчиками. Если вы еще не читали PPP, скажу, что там развивается тема, начатая в этой книге. Прочитавшие убедятся, что многие идеи перекликаются с идеями, изложенными в этой книге на уровне кода.

В этой книге периодически встречаются ссылки на различные принципы проектирования. В частности, упоминается принцип единой ответственности (SRP), принцип открытости/закрытости (OCP) и принцип обращения зависимостей (DIP). Все эти принципы подробно описаны в PPP.

Заключение

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

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

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

Литература

[Beck07]: Implementation Patterns, Kent Beck, Addison-Wesley, 2007.

[Knuth92]: Literate Programming, Donald E. Knuth, Center for the Study of Language and Information, Leland Stanford Junior University, 1992.

Глава 2. Содержательные имена

Тим Оттингер

Имена встречаются в программировании повсеместно. Мы присваиваем имена своим переменным, функциям, аргументам, классам и пакетам. Мы присваиваем имена исходным файлам и каталогам, в которых они хранятся. Мы присваиваем имена файлам jar, war и ear. Имена, имена, имена… Но то, что делается так часто, должно делаться хорошо. Далее приводятся некоторые простые правила создания хороших имен.

Имена должны передавать намерения программиста

Легко сказать: имена должны передавать намерения программиста. И все же к выбору имен следует относиться серьезно. Чтобы выбрать хорошее имя, понадобится время, но экономия окупит затраты. Итак, следите за именами в своих программах и изменяйте их, если найдете более удачные варианты. Этим вы упростите жизнь каждому, кто читает ваш код (в том числе и себе самому).

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

int d; // Прошедшее время

Имя d не передает ровным счетом ничего. Оно не ассоциируется ни с временными интервалами, ни с днями. Его следует заменить другим именем, которое указывает, что именно измеряется и в каких единицах:

int elapsedTimeInDays;

int daysSinceCreation;

int daysSinceModification;

int fileAgeInDays;

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

public List getThem() {

  List list1 = new ArrayList();

  for (int[] x : theList)

    if (x[0] == 4)

      list1.add(x);

  return list1;

}

Почему мы не можем сразу сказать, что делает этот код? В нем нет сложных выражений. Пробелы и отступы расставлены грамотно. В коде задействованы только три переменные и две константы. В нем нет никаких хитроумных классов или полиморфных методов, только список массивов (по крайней мере на первый взгляд).

Проблема кроется не в сложности кода, а в его неочевидности, то есть степени, в которой контекст не следует явно из самого кода. Код подразумевает, что мы знаем ответы на вопросы:

1. Какие данные хранятся в theList?

2. Чем так важен элемент theList с нулевым индексом?

3. Какой особый смысл имеет значение 4?

4. Как будет использоваться возвращаемый список?

Ответы на все эти вопросы не следуют из примера, хотя и могли бы. Допустим, мы работаем над игрой «Сапер». Игровое поле представлено в виде списка ячеек с именем theList. Переименуем его в gameBoard.

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

public List getFlaggedCells() {

  List flaggedCells = new ArrayList();

  for (int[] cell : gameBoard)

    if (cell[STATUS_VALUE] == FLAGGED)

      flaggedCells.add(cell);

  return flaggedCells;

}

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

Можно пойти еще дальше и написать простой класс для представления ячеек вместо использования массива int. В класс включается функция, передающая намерения программиста (назовем ее isFlagged); она скрывает «волшебные» числа. В результате мы получаем новую версию функции:

public List getFlaggedCells() {

  List flaggedCells = new ArrayList();

  for (Cell cell : gameBoard)

    if (cell.isFlagged())

      flaggedCells.add(cell);

  return flaggedCells;

}

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

Избегайте дезинформации

Программисты должны избегать ложных ассоциаций, затемняющих смысл кода. Не используйте слова со скрытыми значениями, отличными от предполагаемого. Например, переменным не стоит присваивать имена hp, aix, and sco, потому что они ассоциируются с платформами и разновидностями Unix. Даже если в переменной хранится длина гипотенузы и имя hp кажется хорошим сокращением, оно может ввести в заблуждение читателя кода.

Не обозначайте группу учетных записей именем accountList, если только она действительно не хранится в списке (List). Слово «список» имеет для программиста вполне конкретный смысл. Если записи хранятся не в List, а в другом контейнере, это может привести к ложным выводам[4]. В этом примере лучше подойдет имя accountGroup, bunchOfAccounts и даже просто accounts.

Остерегайтесь малозаметных различий в именах. Сколько времени понадобится, чтобы заметить незначительное различие в XYZControllerForEfficientHandlingOfStrings в одном модуле и XYZControllerForEfficientStorageOfStrings где-то в другом месте? Эти имена выглядят устрашающе похожими.

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

По-настоящему устрашающие примеры дезинформирующих имен встречаются при использовании строчной «L» и прописной «O» в именах переменных, особенно в комбинациях. Естественно, проблемы возникают из-за того, что эти буквы почти не отличаются от констант «1» и «0» соответственно.