«Хороший код не нуждается в комментариях» — утверждение, которое вызывает бесконечные споры среди разработчиков. Одни считают, что любой комментарий — признак неспособности написать понятный код. Другие уверены, что без подробных пояснений разобраться в чужом (да и своём) коде невозможно. Истина, как обычно, где-то посередине.
Книга Clean Code Роберта Мартина (Uncle Bob) — одна из самых рекомендуемых книг по программированию всех времён — посвящает целую главу комментариям. Её ключевой посыл: «Комментарии не компенсируют плохой код». Но это не означает, что комментарии не нужны вовсе. Это означает, что сначала нужно попытаться написать код, который объясняет себя сам, и только потом добавлять пояснения там, где это действительно необходимо.
В этой статье мы разберём, что такое самодокументируемый код и как его писать, когда комментарии полезны, а когда вредны, как документировать API и публичные интерфейсы, и как работать с TODO, FIXME и другими метками. Вы получите практические примеры и чёткие критерии для принятия решений.
Что такое самодокументируемый код
Самодокументируемый код (self-documenting code) — это код, который настолько понятен и выразителен, что не требует дополнительных пояснений. Его можно прочитать почти как обычный текст и сразу понять, что происходит.
«Имя переменной, функции или класса должно отвечать на все главные вопросы: почему это существует, что это делает и как используется. Если имя требует комментария, значит, оно не раскрывает намерение» — Роберт Мартин, Clean Code
Ключевые принципы
Самодокументируемый код достигается через несколько основных практик. Во-первых, осмысленные имена — переменные, функции и классы названы так, что их назначение очевидно. Во-вторых, маленькие функции — каждая функция делает одну вещь и делает её хорошо. В-третьих, правильные абстракции — код разбит на логические блоки с понятными границами. В-четвёртых, тесты как документация — тесты показывают, как код должен использоваться. И наконец, консистентность — одинаковые вещи называются и делаются одинаково.
Разделение «что» и «как»
Ключевой принцип: разделяйте намерение (что код делает) от реализации (как он это делает). Имя функции должно описывать «что», а тело функции — «как».
Рассмотрим пример плохого кода, где «что» скрыто в «как»:
// Проверяем, можно ли показать кнопкуif (user.age >= 18 && user.country !== 'RU' && user.verified === true && Date.now() - user.createdAt > 86400000) { showButton(); }
Здесь комментарий необходим, потому что из кода непонятно, что проверяется. А вот хороший вариант:
if (canUserAccessPremiumFeatures(user)) { showPremiumButton(); }
Внутри функции canUserAccessPremiumFeatures находится проверка: return isAdult(user) && isInAllowedRegion(user) && isVerified(user) && hasCompletedOnboarding(user);
Во втором примере условие читается почти как обычное предложение. Нам не нужен комментарий, чтобы понять намерение — оно выражено в коде.
Осмысленные имена: фундамент читаемого кода
Имена — это 90% самодокументируемого кода. Хорошее имя устраняет необходимость в комментарии. Плохое имя создаёт необходимость в комментарии, который всё равно не спасёт ситуацию.
Переменные: существительные, описывающие содержимое
| Плохо | Хорошо | Почему |
|---|---|---|
|
| Однобуквенные имена не несут смысла |
|
| «Data» слишком общее слово |
|
| Описывает назначение, не временность |
|
| Булевы переменные — вопрос с ответом да/нет |
|
| Описывает содержимое, не структуру данных |
Функции: глаголы, описывающие действие
Имя функции должно описывать, ЧТО она делает, а не КАК. Используйте глаголы или глагольные фразы.
| Плохо | Хорошо | Почему |
|---|---|---|
|
| «Process» ничего не говорит о результате |
|
| Конкретное действие вместо абстрактного |
|
| Ясно, что функция делает |
|
| Глагол вместо существительного |
Классы: существительные, описывающие сущность
Классы представляют «вещи», поэтому называйте их существительными или фразами с существительными. Плохие имена: Manager, Handler, Processor, Data, Info, Utils, Helper. Хорошие имена: UserRepository, PaymentGateway, EmailSender, CustomerProfile, OrderDetails, DateFormatter, PriceCalculator.
Классы с именами Utils, Helper, Manager, Handler часто становятся свалкой для несвязанного кода. Если не можете придумать конкретное имя — вероятно, класс делает слишком много разных вещей.
Булевы переменные: вопросы с ответом да/нет
Булевы переменные и функции должны читаться как вопросы, на которые можно ответить «да» или «нет». Хорошие примеры: isLoggedIn, hasPermission, canEdit, shouldShowBanner, wasUpdated. Для функций: isValidEmail(), hasActiveSubscription(), canAccessFeature().
Консистентность терминологии
Используйте одни и те же термины для одних и тех же концепций во всей кодовой базе. Если вы называете получение данных fetch, не используйте get, retrieve или load для аналогичных операций.
Антипаттерн: fetchUsers(), getOrders(), retrieveProducts(), loadCategories() — если все эти функции делают одно и то же (получают данные из API), используйте единый глагол: fetchUsers(), fetchOrders(), fetchProducts(), fetchCategories().
Маленькие функции: код как рассказ
Большие функции — враг читаемости. Когда функция занимает 200 строк, понять её назначение без комментариев невозможно. Маленькие функции с хорошими именами читаются как рассказ.
Принцип единственной ответственности
Функция должна делать одну вещь и делать её хорошо. Если вы описываете функцию словами «делает X И Y» — это два кандидата на разделение.
Пример плохой функции, которая делает слишком много:
function processOrder(order) { /* валидация, расчёт цены, применение скидки, сохранение в БД, отправка email — всё в одной функции на 50 строк */ }
Хороший вариант — каждая функция делает одно действие:
function processOrder(order) { validateOrder(order); const total = calculateTotal(order); const finalPrice = applyDiscounts(total, order.coupon); saveOrder(order, finalPrice); notifyCustomer(order); }
Второй вариант читается как план действий. Каждый шаг понятен без погружения в детали реализации.
Уровни абстракции
Код внутри функции должен находиться на одном уровне абстракции. Не смешивайте высокоуровневую логику с низкоуровневыми деталями. Например, если функция createUser вызывает validateUserData() и sendWelcomeEmail(), она не должна содержать код создания соединения с базой данных — это другой уровень абстракции, который нужно вынести в отдельную функцию saveUserToDatabase().
Когда комментарии необходимы
Самодокументируемый код — отличная цель, но это не значит, что комментарии не нужны. Есть ситуации, когда комментарий — единственный способ передать важную информацию.
«Код рассказывает КАК, комментарии рассказывают ПОЧЕМУ» — Джефф Этвуд, сооснователь Stack Overflow
1. Объяснение «почему», а не «что»
Код показывает, что происходит. Комментарий объясняет, почему выбрано именно такое решение.
Пример: // Используем bubble sort вместо quick sort, потому что для массивов < 10 элементов он быстрее из-за меньшего overhead
Без комментария будущий разработчик может «оптимизировать» код, убрав bubble sort, и ухудшить производительность.
2. Объяснение неочевидного поведения
Иногда код делает что-то странное, но на то есть причина. Объясните её.
Пример: // Добавляем 1мс задержку, чтобы обойти race condition в Safari при быстрых переключениях вкладок. См. баг: https://bugs.webkit.org/show_bug.cgi?id=12345
3. Ссылки на внешние источники
Если код реализует алгоритм из статьи, стандарта или решает известный баг — дайте ссылку.
Примеры: // Реализация алгоритма Левенштейна — https://en.wikipedia.org/wiki/Levenshtein_distance или // Workaround для бага в Chrome 120, можно удалить после выхода Chrome 122
4. Предупреждения о последствиях
Если изменение кода может привести к неочевидным проблемам — предупредите.
Пример: // ВНИМАНИЕ: Порядок вызовов критичен! initPayment() должен вызываться ДО loadUserData(), иначе платёжный провайдер не получит корректный session ID.
5. Документация публичного API
Публичные функции, классы и методы библиотек должны быть задокументированы. Пользователи не должны читать исходный код, чтобы понять, как использовать вашу библиотеку. Документируйте параметры, возвращаемые значения, возможные исключения и примеры использования.
6. Бизнес-логика и domain knowledge
Код может быть технически понятен, но непонятен с точки зрения бизнеса. Объясните предметную область.
Пример: // В России НДС 20%, но для некоторых категорий товаров (детские товары, продукты питания) применяется ставка 10%. Категории определены в Налоговом кодексе РФ, ст. 164.
7. Оптимизации производительности
Оптимизированный код часто менее читаем. Объясните, зачем он такой и что вы оптимизировали.
Пример: // Используем побитовые операции вместо Math.floor() для ускорения в 3x на больших массивах. Бенчмарк: https://jsperf.com/math-floor-vs-bitwise
Правило: комментарий оправдан, если будущий разработчик (включая вас через 6 месяцев) может неправильно понять код или попытаться «улучшить» его, сломав что-то.
Когда комментарии вредны
Плохие комментарии хуже, чем их отсутствие. Они вводят в заблуждение, устаревают и создают «шум», затрудняющий чтение кода.
1. Комментарии, дублирующие код
Если комментарий просто пересказывает то, что и так очевидно из кода — он бесполезен.
Плохие примеры: // Увеличиваем счётчик на 1 перед counter++; или // Проверяем, равен ли возраст 18 перед if (age === 18). Код достаточно понятен сам по себе.
2. Комментарии вместо рефакторинга
Если вам нужен комментарий, чтобы объяснить, что делает код — сначала попробуйте переписать код. Вместо комментария «Проверяем, что пользователь взрослый, из разрешённой страны, верифицирован и зарегистрирован более 24 часов назад» перед нечитаемым условием с однобуквенными переменными — вынесите условие в функцию с понятным именем.
3. Закомментированный код
Закомментированный код — это мусор. Используйте систему контроля версий (Git), чтобы хранить историю изменений. Если код не нужен — удалите его. Git помнит всё. Если нужен — раскомментируйте и используйте.
4. Журнальные комментарии
История изменений должна быть в Git, а не в коде. Не пишите комментарии вида «2024-01-15 — Иванов — добавил валидацию email». Для этого есть git log и git blame.
5. Шумовые комментарии
Комментарии ради комментариев только мешают читать код. Не пишите /** Конструктор */ перед конструктором или // возвращаем имя после return this.name;. Код говорит сам за себя.
6. Устаревшие комментарии
Комментарий, который не соответствует коду — хуже, чем его отсутствие. Он активно вводит в заблуждение. Классический пример: комментарий «Отправляем уведомление по email» над кодом, который давно переписан на push-уведомления.
«Неточный комментарий хуже, чем его отсутствие» — Роберт Мартин, Clean Code
Документация API: JSDoc, docstrings и типы
Публичные интерфейсы — исключение из правила «код должен быть самодокументируемым». Пользователи вашей библиотеки или API не должны читать исходный код, чтобы понять, как им пользоваться.
Когда документировать
Обязательно документируйте публичные функции и классы библиотек, а также API endpoints. Желательно документировать публичные методы классов. Приватные методы и внутренние утилиты документируйте только если логика неочевидна.
JSDoc для JavaScript/TypeScript
JSDoc — стандарт документирования JavaScript-кода. Комментарии начинаются с /** и содержат специальные теги.
| Тег | Назначение | Пример |
|---|---|---|
| Параметр функции |
|
| Возвращаемое значение |
|
| Возможные исключения |
|
| Пример использования |
|
| Устаревший метод |
|
| Ссылка на связанный код |
|
| Версия появления |
|
Python docstrings
В Python документация пишется в docstrings — строках сразу после определения функции или класса. Документируйте аргументы в секции Args, возвращаемые значения в Returns, исключения в Raises и добавляйте примеры в Example.
TypeScript: типы как документация
В TypeScript типы частично заменяют документацию. Но JSDoc всё равно полезен для описания семантики и примеров. Типы показывают ЧТО можно передать, комментарии объясняют ЧТО это значит и КАК использовать.
TODO, FIXME, HACK: управление техническим долгом
Специальные метки в комментариях помогают отслеживать технический долг и незавершённую работу. Большинство IDE распознают их и показывают в специальном списке.
Стандартные метки
| Метка | Назначение | Пример |
|---|---|---|
| Запланированная задача |
|
| Известный баг |
|
| Временный workaround |
|
| Что-то сомнительное |
|
| Важное пояснение |
|
| Место для оптимизации |
|
Как правильно использовать TODO
TODO без контекста бесполезен. Через месяц никто не вспомнит, что имелось в виду.
Плохие TODO: // TODO: исправить, // TODO: доделать потом, // TODO: рефакторинг.
Хорошие TODO: // TODO(JIRA-123): добавить rate limiting для защиты от DDoS, // TODO(@ivanov, 2024-03-01): удалить после миграции на новый API, // FIXME(#456): race condition при одновременных запросах.
Лучшие практики для меток
Добавляйте номер тикета — это связывает комментарий с задачей в трекере. Указывайте автора — понятно, к кому обращаться. Ставьте дедлайн — напоминание об истечении срока. Объясняйте контекст — что нужно сделать и почему это не сделано сейчас.
TODO — это технический долг. Каждый TODO без тикета — это задача, которая, вероятно, никогда не будет выполнена. Некоторые команды используют ESLint-правила, запрещающие TODO без номера тикета.
Тесты как документация
Хорошо написанные тесты — это исполняемая документация. Они показывают, как код должен использоваться, какие входы ожидаются и какие результаты должны получиться.
Тесты описывают поведение
Пример хороших тестов:
describe('calculateShipping') содержит тесты: «возвращает базовую стоимость для стандартной доставки», «удваивает цену при экспресс-доставке», «выбрасывает ошибку при весе больше 30 кг», «применяет скидку 10% для расстояния более 500 км».
Читая эти тесты, новый разработчик поймёт: какие параметры принимает функция, как опции влияют на результат, какие есть ограничения и какие есть особые случаи.
Naming conventions для тестов
Имена тестов должны описывать сценарий, а не реализацию.
| Плохо | Хорошо |
|---|---|
|
|
|
|
|
|
Как поддерживать документацию актуальной
Устаревшая документация хуже, чем её отсутствие. Вот как минимизировать риск «гниения» комментариев.
1. Располагайте комментарии близко к коду
Комментарий, расположенный рядом с кодом, который он описывает, с большей вероятностью будет обновлён при изменении кода. JSDoc прямо над функцией обновится скорее, чем документация в отдельном файле docs/api.md.
2. Минимизируйте количество комментариев
Меньше комментариев — меньше того, что нужно поддерживать. Каждый комментарий должен оправдывать своё существование.
3. Используйте линтеры
Некоторые линтеры проверяют актуальность JSDoc: eslint-plugin-jsdoc проверяет соответствие JSDoc и кода, TypeScript выдаёт ошибки, если JSDoc не соответствует типам, pylint проверяет docstrings в Python.
4. Code review с фокусом на комментарии
При code review проверяйте: обновлены ли комментарии при изменении логики, не дублируют ли комментарии код, объясняют ли они «почему» а не «что», нет ли закомментированного кода.
5. Генерируйте документацию из кода
Используйте инструменты, генерирующие документацию из JSDoc/docstrings: JSDoc для JavaScript, TypeDoc для TypeScript, Sphinx для Python, Swagger/OpenAPI для REST API. Когда документация генерируется из кода, она автоматически остаётся актуальной.
Чеклист: когда писать комментарии
Пишите комментарий, если:
Нужно объяснить ПОЧЕМУ код написан именно так (бизнес-требование, баг, ограничение)
Код содержит неочевидный workaround или хак
Есть ссылка на внешний источник (баг-трекер, статья, RFC)
Предупреждение о последствиях изменения кода
Документация публичного API (параметры, возврат, исключения)
Объяснение сложной бизнес-логики или предметной области
Пояснение оптимизации производительности
Регулярное выражение или сложная формула
НЕ пишите комментарий, если:
Комментарий просто пересказывает код
Можно сделать код понятным через рефакторинг
Комментарий компенсирует плохие имена переменных
Это закомментированный код (используйте Git)
Это журнал изменений (используйте Git)
Комментарий описывает очевидные вещи
Перед добавлением комментария спросите себя:
Можно ли переименовать переменную/функцию, чтобы комментарий стал не нужен?
Можно ли вынести логику в отдельную функцию с говорящим именем?
Добавляет ли комментарий информацию, которой НЕТ в коде?
Будет ли этот комментарий понятен через 6 месяцев?
Кто-то обновит этот комментарий при изменении кода?
Заключение
Спор «комментарии vs самодокументируемый код» — ложная дихотомия. Лучший код достигается через умелое сочетание хорошо написанного, самодокументируемого кода и стратегически расставленных комментариев.
Самодокументируемый код — это первая линия защиты. Осмысленные имена, маленькие функции, правильные абстракции делают код читаемым без дополнительных пояснений. Но это не означает, что комментарии не нужны. Код показывает КАК, комментарии объясняют ПОЧЕМУ.
Комментируйте неочевидные решения, workarounds, ссылки на внешние источники, предупреждения о последствиях и публичные API. Не комментируйте очевидные вещи, не дублируйте код словами, не используйте комментарии как оправдание плохому коду.
Помните главное правило: если вам нужен комментарий, чтобы объяснить, что делает код — сначала попробуйте переписать код. Если после рефакторинга комментарий всё ещё нужен — он, вероятно, действительно необходим. И такой комментарий будет ценным для каждого, кто прочитает этот код в будущем, включая вас самих через полгода.
«Код читается гораздо чаще, чем пишется. Пишите код для читателя, а не для компилятора» — Роберт Мартин
А лучшие вакансии для разработчиков ищите на hirehi.ru