Раскрывающие намерение имена
Имя должно отвечать, зачем эта сущность существует, что она делает и как используется, чтобы код читался как его намерение: называй по концепции, а не по типу, убивай слова-шум, заменяй магические числа именованными константами и кодируй единицы измерения.
Ты ревьюишь пулреквест и упираешься в строку: if (d > 30) flag(u). Ты останавливаешься. Что такое d? Что такое 30? Что делает flag и почему тридцать? Ты оставляешь три комментария с вопросами, автор отвечает в Slack, и знание испаряется в тот момент, когда тред прокручивается прочь. Код «работает» — он прошёл тесты, — но каждый читатель после тебя заплатит ту же пошлину вопросами.
Теперь та же строка, названная по намерению: if (daysSinceLastLogin > INACTIVE_THRESHOLD_DAYS) markAccountDormant(account). Спрашивать больше нечего. Имя сделало ту работу, которую пытались сделать комментарий и тред в Slack. В этом весь смысл урока: хорошее имя убирает комментарий и вопрос с ревью. Это самый дешёвый инструмент снижения стоимости изменения, который у тебя есть, и самый недоиспользуемый.
После этого урока ты можешь называть переменную по концепции, которую она представляет, а не по её типу или представлению; срезать слова-шум, не несущие информации; заменять магические числа именованными константами, объясняющими порог; писать булевы как предикаты и кодировать единицы измерения прямо в имени; и распознавать провал — венгерские имена с зашитым типом и слишком длинные имена, просто пересказывающие систему типов, — чтобы остановиться прежде, чем имя станет хуже литерала, который оно заменяет.
Называй по концепции, а не по структуре данных, которая её случайно держит. Имя должно описывать чем эта сущность является в предметной области, а не её представление во время выполнения. Зашивание типа в имя (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), когда коллекция — это подмножество; это различие — реальная информация о предметной области, в отличие от типа контейнера.
Удаляй слова-шум — они раздувают имя, не уточняя его. Слова вроде 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? почему 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. Именование числа ещё и разделяет два литерала, выглядевших одинаково, но означающих разное, — класс багов, который магические числа активно скрывают.
Булевы — это предикаты; кодируй единицы измерения и валюты в имени. Булево должно читаться на точке вызова как вопрос «да/нет»: добавь префикс 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-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.