open atlas
↑ К треку
Паттерны и качество кода CP · 03 · 02

Самодокументируемый код

Комментарий, объясняющий, ЧТО делает код, — обычно имя, которое ещё не извлекли. Перенеси ЧТО в структуру — именованные функции и переменные — и оставь комментарии для ПОЧЕМУ, которое код не несёт: обоснование, ограничения, ссылки.

CP Middle ◷ 19 min
Уровень
ОсновыJuniorMiddleSenior

Ты пишешь комментарий, чтобы помочь: // посчитать итог со скидкой после налога, а дальше двадцать строк арифметики. Полгода спустя арифметика меняется, а комментарий — нет, и теперь комментарий врёт. Ревьюер верит комментарию, пропускает код и одобряет баг. Комментарий, который должен был помочь, превратился во второй источник истины, который никто не держит в синхроне.

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

Цель

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

1

Комментарий, говорящий, что делает код, — это имя, которое ты ещё не написал. Когда ты ловишь себя на том, что пишешь // проверить, может ли пользователь редактировать этот пост над клубком булевых условий, ты уже назвал концепцию — в комментарии. Код под ним — безымянная реализация. Извлечение его в 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)) { /* ... */ }

Комментарий и условие могут разойтись; имя функции и её тело — нет. Что теперь несёт структура, а не проза.

2

Плотный блок, размеченный комментариями-«шагами», — это несколько именованных шагов в маскировке. Классическая форма — одна длинная функция с комментариями // шаг 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 читается так же, как читались комментарии-шаги, — только имена теперь проверены системой типов и переиспользуются везде, где повторяется тот же шаг. Комментарии стали оглавлением, которое функция всегда хотела иметь.

3

Объясняющая переменная превращает загадочное выражение в предложение — функция не нужна. Извлечение — не только про функции. Когда условию или подвыражению нужен // потому что …, чтобы быть читаемым, именование промежуточного значения часто несёт смысл дешевле, чем целая функция.

// 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() говорит это в коде. Объясняющая переменная — самое лёгкое из доступных извлечений: ни нового вызова, ни прыжка в другой файл, просто имя.

4

Комментарии, которые выживают, — это ПОЧЕМУ, и самодокументируемый код делает их заметнее, а не реже. Когда что несут имена, остаются только те комментарии, которые код структурно выразить не может: почему было принято неожиданное решение, ссылка на тикет или 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-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.