Самодокументируемый код
Комментарий, объясняющий, ЧТО делает код, — обычно имя, которое ещё не извлекли. Перенеси ЧТО в структуру — именованные функции и переменные — и оставь комментарии для ПОЧЕМУ, которое код не несёт: обоснование, ограничения, ссылки.
Ты пишешь комментарий, чтобы помочь: // посчитать итог со скидкой после налога, а дальше двадцать строк арифметики. Полгода спустя арифметика меняется, а комментарий — нет, и теперь комментарий врёт. Ревьюер верит комментарию, пропускает код и одобряет баг. Комментарий, который должен был помочь, превратился во второй источник истины, который никто не держит в синхроне.
Большинство поясняющих комментариев такие: они описывают что делает код, прозой, прямо рядом с кодом, который уже говорит, что он делает, — просто менее читаемо. Комментарий, описывающий что, почти всегда — имя, которое ты ещё не извлёк. «Самодокументируемый код» — это дисциплина переноса этого что из комментариев в структуру самого кода, чтобы описание не могло разойтись с тем, что оно описывает.
После этого урока ты можешь отличить что-комментарий (имя, ждущее извлечения) от почему-комментария (обоснование, которое код не выразит); заменить комментарий «вычислить X», извлекая функцию с хорошим именем или объясняющую переменную; распознать сбойный режим, когда извлечение однострочников исключительно ради удаления комментариев меняет понятный блок на запутанную косвенность; и решить, какие комментарии заслуживают пережить рефакторинг — те, что несут обоснование, ограничения и ссылки, которые код структурно выразить не может.
Комментарий, говорящий, что делает код, — это имя, которое ты ещё не написал. Когда ты ловишь себя на том, что пишешь // проверить, может ли пользователь редактировать этот пост над клубком булевых условий, ты уже назвал концепцию — в комментарии. Код под ним — безымянная реализация. Извлечение его в canEdit(user, post) переносит имя из комментария (который компилятор игнорирует и никто не рефакторит) в сигнатуру функции (которую компилятор проверяет и каждый вызывающий читает).
// before: имя живёт в комментарии, который компилятор не может проверить
// check if the user can edit this post
if (user.id === post.authorId || user.role === "admin") { /* ... */ }
// after: имя живёт в коде, который читает сам себя
if (canEdit(user, post)) { /* ... */ }Комментарий и условие могут разойтись; имя функции и её тело — нет. Что теперь несёт структура, а не проза.
Плотный блок, размеченный комментариями-«шагами», — это несколько именованных шагов в маскировке. Классическая форма — одна длинная функция с комментариями // шаг 1: …, // шаг 2: …, разбивающими её на фазы. Эти комментарии — признание, что блок делает несколько вещей и ты почувствовал нужду их подписать. Каждая подпись — имя функции, просящееся существовать.
// before — комментарии пересказывают блок, который делает слишком много
function publish(post: Post) {
// validate the post is ready
if (!post.title || !post.body) throw new Error("incomplete");
if (post.body.length < 100) throw new Error("too short");
// render markdown to html and strip scripts
const html = sanitize(renderMarkdown(post.body));
// write to the store and bump the index
store.save({ ...post, html, status: "live" });
searchIndex.add(post.id, post.title);
}
// after — каждый комментарий-шаг становится именем, которое читает само себя
function publish(post: Post) {
assertPublishable(post);
const html = renderSafeHtml(post.body);
persistAndIndex(post, html);
}Второй publish читается так же, как читались комментарии-шаги, — только имена теперь проверены системой типов и переиспользуются везде, где повторяется тот же шаг. Комментарии стали оглавлением, которое функция всегда хотела иметь.
Объясняющая переменная превращает загадочное выражение в предложение — функция не нужна. Извлечение — не только про функции. Когда условию или подвыражению нужен // потому что …, чтобы быть читаемым, именование промежуточного значения часто несёт смысл дешевле, чем целая функция.
// before: комментарий подпирает нечитаемое условие
// only retry on transient network failures, not 4xx
if (err.code === "ECONNRESET" || err.code === "ETIMEDOUT" || (err.status >= 500 && err.status < 600)) {
retry();
}
// after: имена заставляют условие заявить собственное намерение
const isNetworkReset = err.code === "ECONNRESET" || err.code === "ETIMEDOUT";
const isServerError = err.status >= 500 && err.status < 600;
const isTransient = isNetworkReset || isServerError;
if (isTransient) retry();Комментарий // retry только на временных сбоях теперь избыточен — if (isTransient) retry() говорит это в коде. Объясняющая переменная — самое лёгкое из доступных извлечений: ни нового вызова, ни прыжка в другой файл, просто имя.
Комментарии, которые выживают, — это ПОЧЕМУ, и самодокументируемый код делает их заметнее, а не реже. Когда что несут имена, остаются только те комментарии, которые код структурно выразить не может: почему было принято неожиданное решение, ссылка на тикет или RFC, который его оправдывает, неочевидное ограничение, обходной приём для бага в чём-то, что ты не контролируешь. Это золото, потому что никаким переименованием их не вывести из кода — они кодируют контекст, живущий вне него.
// ПОЧЕМУ выживает: код не может объяснить себя сам
// Stripe rounds half-up; we round half-even to match Finance's ledger.
// Changing this silently re-bills every subscription. See FIN-2231.
const cents = bankersRound(amount * 100);
// Safari <16 throws on structuredClone of a Blob; fall back. (bug: WK-204931)
const copy = canStructuredClone ? structuredClone(v) : deepCopy(v);«Самодокументируемый» не значит «без комментариев». Это значит, что код несёт что так ясно, что выжившие почему-комментарии выделяются, а не тонут в шуме вроде // increment i. Senior-ревьюер читает файл, и каждый оставшийся комментарий заслуживает своё место.
Одна неаккуратная функция, с комментариями и всем прочим, отрефакторена так, чтобы читать саму себя, — при этом сохранён единственный значимый комментарий. Вот калькулятор комиссии, пересказанный комментариями-шагами, с настоящим почему, зарытым среди них:
function settlementFee(order: Order): number {
// step 1: base fee is 2.9% + 30c, like the card networks
let fee = order.total * 0.029 + 30;
// step 2: high-risk regions get a surcharge
if (order.region === "BR" || order.region === "NG" || order.region === "ZA") {
fee += order.total * 0.01;
}
// step 3: cap the fee — legal limit in the EU is 1.5% (PSD2), don't exceed
if (order.region.startsWith("EU") && fee > order.total * 0.015) {
fee = order.total * 0.015;
}
return Math.round(fee);
}Три комментария-шага пересказывают что; один фрагмент (legal limit in the EU is 1.5% (PSD2)) — настоящее почему. Перенеси что в имена и дай почему выжить как единственному комментарию:
function settlementFee(order: Order): number {
const fee = applyRiskSurcharge(baseFee(order), order);
return Math.round(capForRegion(fee, order));
}
function baseFee(order: Order): number {
return order.total * 0.029 + 30; // card-network standard: 2.9% + 30c
}
function applyRiskSurcharge(fee: number, order: Order): number {
return isHighRiskRegion(order.region) ? fee + order.total * 0.01 : fee;
}
function capForRegion(fee: number, order: Order): number {
// PSD2 caps EU interchange at 1.5%; exceeding it is a legal violation, not just a bug. See PAY-884.
if (!order.region.startsWith("EU")) return fee;
return Math.min(fee, order.total * 0.015);
}Комментарии «step 1/2/3» исчезли — теперь это имена функций, которые читаются по порядку внутри settlementFee. Список регионов высокого риска стал isHighRiskRegion, именованным предикатом, переиспользуемым в другом месте. А единственный комментарий, который не мог стать именем, — правовое ограничение PSD2 — не только выжил, но и стал чётче: читатель теперь знает, что лимит — это закон, а не настройка, плюс тикет, за которым гнаться. В этом весь приём: структура поглощает что, почему остаётся стоять в одиночестве там, где его невозможно пропустить.
▸Почему это работает
Почему перенос что в имена бьёт хороший комментарий, когда оба сегодня читаются ясно? Потому что они деградируют по-разному. Имя несущее: вызывающие на него ссылаются, проверяльщик типов его валидирует, инструмент переименования обновляет каждое использование, а тело не может молча разойтись с сигнатурой, которую люди вызывают. Комментарий инертен: на него ничто не ссылается, ничто его не проверяет, и у следующего человека, который правит код тремя строками ниже, нет механической причины обновлять прозу тремя строками выше. Так что комментарий и его код расходятся по умолчанию, тогда как имя и его тело остаются связанными по построению. Ты выбираешь не между двумя одинаково хорошими описаниями — ты выбираешь между описанием, которое инструментарий держит честным, и тем, которое он игнорирует.
▸Частая ошибка
Senior-сбойный режим — это перекоррекция: извлечение однострочной функции исключительно ради удаления комментария, и в итоге становится хуже. // add tax над total + total * rate не оправдывает function totalWithTax(...), если она вызывается один раз и встроенная форма уже читается нормально, — ты сменил строку, понятную с одного взгляда, на прыжок в другое место, лишнее имя для запоминания и функцию, существующую лишь для того, чтобы приютить комментарий смысла. У косвенности реальная цена: каждое извлечённое имя — это то, что надо прочитать, найти и держать согласованным. Извлекай, когда имя снимает настоящую когнитивную нагрузку (запутанное условие, повторяющийся шаг, что-комментарий над реальным блоком). Не извлекай, когда встроенный код уже предложение, а функция была бы просто слабо замаскированным комментарием с местом вызова. Самодокументируемый код — это меньше комментариев и правильное количество структуры, а не максимум структуры.
Ты видишь `// reject orders from sanctioned countries` над списком проверок кодов стран, а тремя строками позже `// using FNV-1a here because the default hash collided badly on our short keys (see PERF-77)`. Рефакторя в сторону самодокументируемого кода, что ты делаешь с каждым комментарием?
Комментарий, объясняющий, что делает код, — обычно имя, ждущее извлечения: // check if the user can edit становится canEdit(user, post), // step 2 становится именованной функцией, нечитаемое условие становится цепочкой объясняющих переменных. Перенос что в структуру бьёт комментирование, потому что имя несущее и проверяется инструментарием, а комментарий инертен и расходится. Комментарии, которые выживают, — это почему: обоснование, ссылки на тикеты и RFC, неочевидные ограничения, неожиданные решения, — которое код структурно выразить не может; самодокументируемый код делает их заметнее, а не отсутствующими. Сбойный режим — это переизвлечение: плодить однострочные функции исключительно ради удаления комментария, меняя понятный с одного взгляда блок на косвенность, за которой труднее следить. Тест, как всегда: делает ли это изменение следующее изменение следующего читателя дешевле — или просто переставляет его?
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.