Привет!
Сегодня вашему вниманию предлагается весьма дискуссионная статья, затрагивающая важный аспект философии "Чистого Кода". Автор статьи берет на себя смелость утверждать, что в большинстве случаев комментарии в коде вредны, но также не забывает указать, когда без них не обойтись.
Поэтому — читаем внимательно и, несмотря ни на что, комментируем.
Чистый код должен читаться как хорошо написанная проза — Роберт Мартин
Как правило, прослеживается выраженная корреляция между плохим кодом и кодом, переполненным комментариями. Комментарии – самый характерный признак беспорядочного исходного кода.
Каждый программист должен стремиться писать настолько чистый и выразительный код, чтобы комментарии в нем просто не требовались. Смысл каждой переменной, функции и класса должны распознаваться по их названию и структуре. Если вам требуется написать комментарий – это обычно означает, что ваш код получился недостаточно выразительным. Всякий раз, когда вы пишете комментарий, вас должны пробирать угрызения совести.
Когда кто-то читает ваш код, ему и без комментариев должно быть понятно, что этот код делает. Грамотно поименованные классы и функции должны помогать читателю следить за развитием событий, словно это не код, а хороший роман. Когда читатель встретит новую функцию или класс, их содержимое не должно его удивлять. Помните: львиная доля рабочего времени программиста тратится не на написание кода, а на чтение чужого кода, в котором приходится разбираться.
Комментарии маскируют косяки
Мне часто приходится встречать комментарии над названиями переменных или функций; такие комментарии описывают, что делает код (или должен делать). Такие комментарии явственно свидетельствуют, что программист не смог подобрать достаточно выразительное название, либо что данная функция делает более одной вещи.
Правильное именование сущностей в коде – исключительно важная вещь. Всеми силами старайтесь называть каждый фрагмент кода настолько точно, чтобы другие разработчики понимали его с первого раза и однозначно.
// найти сотрудников по статусу
List<Employee> find(Status status) {
...
}
В данном примере название find
получилось недостаточно информативным, поэтому автору функции пришлось компенсировать это при помощи комментария, описывающего, что делает эта функция. Однако, если функцию find доведется вызывать из другого модуля, то останется лишь догадываться, что она находит. Что именно понимается в данном случае под «находить»? Как она находит что бы то ни было? Возвращает ли она найденное? Как указывает Дядюшка Боб в своей книге «Чистый код», если вам требуется написать комментарий – значит, вы не смогли как следует выразить мысли в коде.
Кому понравится вникать в комментарий над каждой функцией, чтобы разобраться, что она делает?
List<Employee> getEmployeesByStatus(Status status) {
...
}
Теперь назначение этой функции очевидно уже из ее сигнатуры, и комментарий становится избыточным. Итак, я подхожу к следующему аспекту, поясняющему, почему комментарии сопутствуют недоработкам.
Избыточные комментарии
Они только замусоривают ваш код и совершенно не нужны. Если вы наполните код кучей лишних комментариев – то человек, читающий его, приучится пропускать все комментарии до единого, и из-за этого, вероятно, не прочитает те, которые действительно важны.
// Эта функция отправляет электронное сообщение
void sendEmail() {
...
}
// В этом классе содержатся данные по сотруднику
public class Employee {
...
}
/**
* @param title Это заголовок CD
* @param author Это автор CD
* @param tracks Количество треков на CD
*/
public void addCd(String title, String author, int tracks) {
...
}
В последнем примере показана избыточность, спускаемая сверху. Во многих организациях требуют подписывать таким образом каждую функцию и класс. Если ваш начальник тоже так поступает – попросите его так не делать.
Неверный уровень абстрагирования
Если у вас есть длинная функция, либо вам требуется документировать код, показав, что в какой части делается, то, возможно, вы нарушаете следующие правила:
- Каждая функция должна делать ровно одну вещь.
- Функции должны быть маленькими.
Вот пример
// Эта функция считает прайсы, сравнивает их с продажами,
// промо-акциями, проаеряет, верны ли цены, а затем
// отправляет пользователю письмо с промо-предложением
public void doSomeThings() {
// Вычислить прайсы
...
...
...
// Сравнить вычисленные прайсы с промо-показателями по продажам
...
...
...
// Проверить, верны ли вычисленные прайсы
...
...
...
// Отправить пользователям промо-предложения
...
...
...
}
Если у вас в коде есть фрагменты, которые можно выделить в самостоятельные функции – то нужен рефакторинг. Если вам удалось инкапсулировать каждый фрагмент логики в отдельную функцию, то код должен читаться именно как описание заложенных в ним действий.
После рефакторинга имеем:
public void sendPromotionEmailToUsers() {
calculatePrices();
compareCalculatedPricesWithSalesPromotions();
checkIfCalculatedPricesAreValid();
sendPromotionEmail();
}
Вместо комментирования каждого фрагмента вашего кода добейтесь, чтобы любой фрагмент логики красиво инкапсулировался в собственную функцию.
Во-первых, так повышается удобочитаемость. Код не приходится читать подряд, строку за строкой. Достаточно будет прочитать название вспомогательной функции – и понять, что она делает. Если нам требуется подробнее разобраться в деталях той или иной функции, то всегда можно заглянуть в ее реализацию.
Во-вторых, так улучшается тестируемость. В вышеприведенном примере можно провести отдельный тест для каждой функции. Если не инкапсулировать эти отдельные функции, то будет сложно проверить каждую часть более крупной функции sendPromotionEmailToUsers()
. Чем больше разных вещей делает функция, тем сложнее ее тестировать.
Наконец, рефакторинг такого кода также упрощается. Если мы инкапсулируем каждый элемент логики в собственную функцию, то в дальнейшем изменения в нее будет проще вносить, причем, они будут сказываться лишь на поведении этой функции. Если у нас будут длинные функции с локальными переменными, существующими на протяжении всего выполнения функции, то рефакторинг такой функции спровоцирует изменения где-то еще из-за сильной связанности кода.
Закоментированный код
Закоментированный код следует считать опасным для жизни. Не смотрите в него, не нюхайте его, не спрашивайте, откуда он взялся – а просто избавляйтесь от него. Чем дольше он остается в программе, тем дольше вся программа будет с душком.
/*
public void oldFunction() {
noOneRemembersWhyIAmHere();
tryToUnCommentMe();
iWillProbablyCauseABuildFailure();
haHaHa();
}
*/
Если попытаться раскомментировать такой код – кто знает, скомпилируется ли он вообще? Не выведет ли этот фрагмент из строя какую-то другую часть кода? Просто удаляйте. Если этот код потребуется вам позже, то вы всегда сможете взять его в системе контроля версий – вы же пользуетесь системой контроля версий, не правда ли?
TODO-комментарии
Не пишите TODO-комментариев, а лучше просто… сделайте то, что собирались? Как правило, такие комментарии просто забываются, а позже могут стать неактуальными или ложными. Когда другой кодер увидит TODO-комментарий, откуда ему знать, сделана ли уже эта операция? Такие комментарии тоже удаляйте.
Единственный случай, в котором TODO-комментарий представляется уместным – когда вы ждете мерджа от коллеги. Предполагается, что такой комментарий провисит в коде недолго, только до тех пор, пока вы сможете внести в код исправления и зафиксировать их.
Если вы чувствуете, что нужно написать комментарий – сначала попробуйте сделать рефакторинг, так, чтобы любые комментарии в коде стали излишними. — Мартин Фаулер
Комментарии врут
Еще одна проблема с комментариями заключается в том, что они лживы.
Когда Джимми оставляет комментарий над новой функцией, которую только что собственноручно написал – он думает, что помогает другим разработчикам, которые впоследствии будут читать его код. На самом же деле он ставит капкан. Его комментарий может месяцы и годы пролежать в неприкосновенности, пока не превратится в гнусную ложь. Затем, однажды, после сотни рефакторингов и изменений требований, этот комментарий будет дезавуирован каким-нибудь далеким модулем.
Когда вы переписываете строку кода, откуда вам знать, не отменяете ли вы тем самым комментарий в какой-нибудь другой части кода? Этого никак не узнаешь. Поэтому комментарии должны быть искоренены.
public class User {
...
// Здесь содержится имя и фамилия пользователя
String name;
...
}
Позже разработчик решает разделить name
на firstName
и lastName
.
// Обрабатываем информацию о сотруднике в зависимости от статуса
void processEmployees() {
...
List<Employee> employees = findEmployees(statusList);
...
}
// находим сотрудников по списку статусов
List<Employee> findEmployees(List<String> statusList) {
...
}
Бах! Комментарий неверен. Можно было бы обновить комментарий, чтобы он отражал внесенные поправки, но вам правда хочется вручную поддерживать все комментарии после любых вносимых изменений? Вы же разработчик, а не документировщик.
Но такой комментарий легко заметить и можно без проблем изменить.
Рассмотрим другой пример:
// Обрабатываем сотрудников в зависимости от статуса
void processEmployees() {
...
List<Employee> employees = findEmployees(statusList);
...
}
// здесь мы находим сотрудников по списку статусов
List<Employee> findEmployees(List<String> statusList) {
...
}
Позже кто-нибудь может изменить функцию findEmployees так, что она будет находить сотрудников по списку имен, а не по списку статусов.
// Обрабатываем сотрудников в зависимости от их статуса
void processEmployees() {
...
List<Employee> employees = findEmployees(statusList);
...
}
// здесь мы находим сотрудников по списку статусов
List<Employee> findEmployees(List<String> nameList) {
...
}
Сначала стал неактуален комментарий над findEmployees, и его нужно изменить. Это не проблема, так? Не так.
Комментарий над processEmployees
также стал неактуален, и его тоже необходимо изменить. Сколько еще таких комментариев стали недействительны в результате такого маленького рефакторинга?
Альтернативное решение:
void processEmployees() {
...
List<Employee> employees = findEmployeesByName(nameList);
...
}
List<Employee> findEmployeesByName(List<Name> nameList) {
...
}
Если точно и внимательно именовать функции, то никаких комментариев не понадобится, и по вашему коду не будет расползаться ложь.
Когда комментарии – это нормально
Среди разработчиков мне известно множество фанатичных сторонников подробного комментирования кода. Полемизируя с ними, я вынужден согласиться, что иногда комментарии – это нормально. Всякий раз, занося в код комментарий, мы делаем это скрепя сердце, но порой комментарий – необходимое зло…
Сложные выражения
Если у вас в коде есть сложная SQL-инструкция или регулярное выражение – сопроводите их комментарием. Подобные команды обычно бывает тяжело чисто и выразительно изложить в коде. Комментарий над таким выражением серьезно поможет коллегам-разработчикам понять ваш код.
// формат по шаблону kk:mm:ss EEE, MMM dd, yyy
Pattern timePattern = Pattern.compile("\d*:\d*:\d* \w*, \w*, \d*, \d*");
Предупреждения
Если вам требуется предупредить других разработчиков о возможных побочных эффектах или неприятностях, то такой код уместно сопроводить комментарием. Подобные комментарии могут сработать как маячки, предупреждающие о таинственном поведении программы – поэтому код с ними становится гораздо ценнее.
Пояснение намерений
Если написать чистый код не получается – признайте это и напишите комментарий. Вы отвечаете за весь код, который пишете, поэтому никогда не оставляйте плохой код без пояснений.
Если написать комментарий необходимо, то убедитесь, что он стоит на нужном месте. Комментарий, расположенный далеко от того кода, на который ссылается, скоро превратится в ложь, поэтому подлежит уничтожению. Комментарий, описывающий функцию или переменную, должен находиться непосредственно над ней. Если в вашей IDE поддерживается подсвечивание комментариев, добейтесь, чтобы ваши комментарии хорошо выделялись на фоне кода.
Автор: ph_piter