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

Раскрывающие намерение имена

Имя должно отвечать, зачем эта сущность существует, что она делает и как используется, чтобы код читался как его намерение: называй по концепции, а не по типу, убивай слова-шум, заменяй магические числа именованными константами и кодируй единицы измерения.

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

Ты ревьюишь пулреквест и упираешься в строку: if (d > 30) flag(u). Ты останавливаешься. Что такое d? Что такое 30? Что делает flag и почему тридцать? Ты оставляешь три комментария с вопросами, автор отвечает в Slack, и знание испаряется в тот момент, когда тред прокручивается прочь. Код «работает» — он прошёл тесты, — но каждый читатель после тебя заплатит ту же пошлину вопросами.

Теперь та же строка, названная по намерению: if (daysSinceLastLogin > INACTIVE_THRESHOLD_DAYS) markAccountDormant(account). Спрашивать больше нечего. Имя сделало ту работу, которую пытались сделать комментарий и тред в Slack. В этом весь смысл урока: хорошее имя убирает комментарий и вопрос с ревью. Это самый дешёвый инструмент снижения стоимости изменения, который у тебя есть, и самый недоиспользуемый.

Цель

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

1

Называй по концепции, а не по структуре данных, которая её случайно держит. Имя должно описывать чем эта сущность является в предметной области, а не её представление во время выполнения. Зашивание типа в имя (accountList, accountArray, userMap) привязывает имя к решению, которое часто меняется, — и когда ты заменишь Array на Set, имя начинает врать.

// тип зашит: имя следит за контейнером, а не за концепцией
const accountList: Account[] = await fetchAccounts();
for (const a of accountList) { /* ... */ }

// названо по концепции: читается как предметная область, переживает смену представления
const accounts: Account[] = await fetchAccounts();
for (const account of accounts) { /* ... */ }

accounts стоит во множественном числе, поэтому уже подразумевает коллекцию — суффикс List/Array добавляет ноль информации и одну обузу сопровождения. Называй роль (activeAccounts, accountsToCharge), когда коллекция — это подмножество; это различие — реальная информация о предметной области, в отличие от типа контейнера.

2

Удаляй слова-шум — они раздувают имя, не уточняя его. Слова вроде data, info, value, object, manager, processor и избыточный пересказ контекста (user.userId, Account.accountName) не несут информации, которую окружающий код уже не давал бы. Если удаление слова не меняет, на какую сущность ссылается имя, — это слово шум.

// шум: каждый суффикс пересказывает контекст, уже бывший в области видимости
interface UserData {
  userId: string;
  userName: string;
  userAccountInfo: AccountInfo;
}
const userDataManager = new UserDataManager();

// сигнал: тип И ЕСТЬ контекст; поля его не повторяют
interface User {
  id: string;
  name: string;
  account: Account;
}
const users = new UserRepository();

user.id однозначно — ты уже знаешь, что это id пользователя, из переменной. UserDataManager — худший нарушитель: Data и Manager оба заглушки для «я не решил, что это делает». Имя, которое не желает зафиксировать ответственность, — это запах дизайна, носящий ярлык.

3

Заменяй магические числа именованными константами — имя несёт зачем. Голый литерал заставляет каждого читателя реверс-инжинирить его смысл, а продублированный литерал скрывает, какие вхождения — одна и та же концепция. Именованная константа превращает значение в искабельный факт с единственным источником истины и бесплатно документирует намерение.

// магия: что такое 3? почему 86400000? эти две пятёрки — одна и та же 5?
if (attempt > 3) lock();
if (Date.now() - issuedAt > 86400000) expire();

// именовано: каждая константа отвечает «почему именно это число?»
const MAX_RETRIES = 3;
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
if (attempt > MAX_RETRIES) lock();
if (Date.now() - issuedAt > ONE_DAY_MS) expire();

Выигрыш двойной: читатель понимает порог без комментария, а будущее изменение («повторов → 5») — это одна правка в определении, а не охота-и-замена по точкам вызова, которые могли совпасть с несвязанной 3. Именование числа ещё и разделяет два литерала, выглядевших одинаково, но означающих разное, — класс багов, который магические числа активно скрывают.

4

Булевы — это предикаты; кодируй единицы измерения и валюты в имени. Булево должно читаться на точке вызова как вопрос «да/нет»: добавь префикс is, has, should или can, чтобы if (expired) стало if (isExpired), а параметр flush стал shouldFlush. И любая величина с единицей измерения должна говорить свою единицу в имени — timeoutMs, priceCents, distanceKm, sizeBytes, — потому что именно в единице прячутся продакшен-баги.

// неоднозначно: `timeout` в секундах или мс? `price` в долларах или центах?
function retry(timeout: number, price: number, active: boolean) {
  setTimeout(fn, timeout); // ...а вызывающий передаёт секунды? БАБАХ.
}

// безошибочно: единица и полярность — в имени
function retry(timeoutMs: number, priceCents: number, isActive: boolean) {
  setTimeout(fn, timeoutMs);
}

setTimeout хочет миллисекунды; вызывающий, передающий 30 с мыслью «30 секунд», вносит баг, который не ловит ни один проверяльщик типов, — number есть number. timeoutMs делает контракт невозможным для неверного прочтения, и баг с ошибкой в 1000 раз просто не пишется. Единицы в именах — не педантизм; это разница между таймаутом в 30мс и 30с в проде.

Разбор примера

Отрефактори блок так, чтобы он читался как своё намерение. Вот функция реалистичной формы, которая «работает», но заставляет читателя расшифровывать себя:

function chk(arr: Order[], t: number): Order[] {
  const r: Order[] = [];
  for (const o of arr) {
    // 1209600000 = две недели в мс
    if (Date.now() - o.ts > 1209600000 && o.s !== 2 && t > 0) {
      r.push(o);
    }
  }
  return r;
}

Каждое имя — загадка: chk, arr, t, r, o.s !== 2 и магическое 1209600000, спасаемое лишь комментарием, который может протухнуть. Чтобы понять предикат, нужно держать в голове пять неизвестных разом. Теперь назови всё по намерению:

const TWO_WEEKS_MS = 14 * 24 * 60 * 60 * 1000;

function findStaleUnshippedOrders(orders: Order[], retryBudget: number): Order[] {
  const hasRetryBudget = retryBudget > 0;
  return orders.filter((order) => {
    const ageMs = Date.now() - order.createdAtMs;
    const isOlderThanTwoWeeks = ageMs > TWO_WEEKS_MS;
    const isUnshipped = order.status !== OrderStatus.Shipped;
    return isOlderThanTwoWeeks && isUnshipped && hasRetryBudget;
  });
}

Имя функции сообщает, что она возвращает; константа объясняет порог и убивает комментарий; o.s !== 2 становится order.status !== OrderStatus.Shipped (магическое значение перечисления тоже названо); булевы-посредники превращают условие в предложение. Заметь, что исчезло: поясняющий комментарий и каждое «что это?», которое спросил бы ревьюер. Поведение байт-в-байт идентично — изменилось лишь то, что код теперь сам говорит, зачем он существует.

Почему это работает

Почему именование — самый senior-ский дешёвый выигрыш на ревью? Потому что имя читают при каждом будущем визите, а пишут единожды. Ревьюер, упирающийся в isExpired, читает ноль комментариев и задаёт ноль вопросов; ревьюер, упирающийся в d и 30, платит фиксированный налог — прочитать, угадать, спросить, дождаться ответа, — и так же делает следующий ревьюер, и тот, кто будет онбордиться через год. Автор заплатил пять секунд, чтобы выбрать INACTIVE_THRESHOLD_DAYS; эта покупка продолжает приносить отдачу всю жизнь кода. Хорошие имена — вложение с наивысшим отношением чтения к записи, которое ты делаешь, и именно поэтому призма стоимости изменения ставит их на первое место.

Частая ошибка

Провал — это перекоррекция в имена, пересказывающие систему типов. Венгерская нотация (strName, bIsActive, arrAccounts, iCount) кодирует тип, который компилятор уже знает, — и протухает в тот миг, когда тип меняется. Не менее плохо слишком длинное имя, переписывающее сигнатуру: processTheListOfUserAccountObjectsAndReturnActiveOnes добавляет слова, а не ясность, там, где selectActive(accounts) всё уже сказал. Цель не «побеждает самое длинное имя»; это кратчайшее имя, полностью раскрывающее намерение. Если слово не меняет, что именно ты имеешь в виду, или просто повторяет тип, — удали его. Имя, воюющее с проверяльщиком типов или изматывающее читателя, хуже краткого литерала, который оно заменяет.

Проверь себя
Викторина

Ревьюер видит `function send(arr: Msg[], t: number, f: boolean)`. Какое переименование лучше всего следует раскрывающему намерение именованию для этого трека?

Итог

Раскрывающее намерение имя отвечает, что это, зачем оно существует и как используется, чтобы код читался как своя цель, — и каждое такое имя снимает комментарий и вопрос с ревью. Называй по концепции, а не по типу контейнера (accounts, а не accountList); срезай слова-шум, лишь пересказывающие контекст (user.id, а не user.userId; никогда …Manager/…Data); заменяй магические числа именованными константами, несущими зачем (MAX_RETRIES, ONE_DAY_MS); пиши булевы как предикаты (isExpired, hasAccess) и кодируй единицы измерения там, где прячутся баги (timeoutMs, priceCents); и предпочитай искабельные имена одиночным буквам. Провал — это перекоррекция: венгерские имена с зашитым типом и слишком длинные имена, пересказывающие систему типов, хуже литерала — цель в кратчайшем имени, полностью раскрывающем намерение. Именование — ход с наивысшим рычагом в этом треке, потому что его читают вечно, а пишут единожды.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

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

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

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

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

Trademarks belong to their respective owners. Editorial reference only.