Согласованность и область видимости
Одно слово на одну концепцию по всей кодовой базе — смешение синонимов это скрытая связанность; и длина имени растёт вместе с областью видимости: лаконичные имена в крошечных локальных, описательные на границах модулей.
Ты открываешь сервис и читаешь getUser, через три файла — fetchUser, потом retrieveUserById, потом loadCustomer. Это четыре операции над четырьмя концепциями или одна операция над одной концепцией, записанная четырьмя способами? По именам не понять — поэтому ты идёшь читать все четыре реализации, чтобы выяснить, что это одно и то же. Этот налог на чтение платит каждый, кто когда-либо тронет этот код, — всегда.
Имя — это не просто ярлык на одном символе. Это обещание о том, как этот символ связан со всеми остальными именами в системе. Когда словарь несогласован, читателю приходится выучить, что fetchX и getX значат одно и то же, — и это выученное соответствие есть связанность, живущая у него в голове, а не в коде.
После этого урока ты сможешь применять две силы именования, которые senior-инженер постоянно балансирует: держать одно слово на одну концепцию по всей кодовой базе (общий, единый словарь — это форма низкой связанности) и масштабировать длину имени под область видимости (лаконично там, где читатель видит всё, описательно там, где не видит). Ты также сможешь назвать режим отказа каждого правила — когда насильственная согласованность или насильственная описательность активно делают код хуже.
Одно слово на одну концепцию — синонимы это скрытая связанность. Выбери ровно один глагол для операции и одно существительное для доменной сущности, а затем используй их везде. Если «получить пользователя» это getUser, то это getUser в репозитории, в сервисе, в контроллере и в тесте — никогда не fetchUser здесь и retrieveUser там. То же касается существительных: если домен называет сущность Customer, то это Customer от начала и до конца, а не User в аутентификации, Customer в биллинге и Account в дашборде.
// Inconsistent vocabulary — three words, one concept
const a = await userRepo.fetchUser(id);
const b = await userService.getUser(id);
const c = await cache.retrieveUserById(id);Читатель, пробегающий это глазами, не может предположить, что fetch, get и retrieve взаимозаменяемы — разные слова сигнализируют о разном поведении. Поэтому ему приходится открыть каждое, чтобы подтвердить: «всё это просто чтение по ключу». Этот шаг подтверждения — чистые потери, и они растут с каждым синонимом, который ты вводишь.
Общий словарь — это низкая связанность; расщеплённый словарь — это конкретность смысла. Когда User, Customer и Account обозначают одно и то же, каждый читатель и каждый инструмент рефакторинга обязан держать в голове соответствие User ≡ Customer ≡ Account. Это общее понимание — невидимая зависимость между модулями: измени, что значит Customer в биллинге, и ты молча изменил допущение, на которое опирается модуль аутентификации, потому что они были «одним и тем же» лишь по договорённости, а не по типу.
// Three names, one entity → an unwritten contract readers must memorise
function authenticate(user: User): Session { /* ... */ }
function chargeCard(customer: Customer): Receipt { /* ... */ }
function renderHeader(account: Account): Html { /* ... */ }Один единый термин (скажем, Customer повсюду) схлопывает три ментальные модели в одну и позволяет системе типов обеспечивать связь вместо памяти читателя. Именно поэтому предметно-ориентированное проектирование продвигает единый язык: общие слова — это самое дешёвое снижение связанности, какое можно купить.
Длина имени должна масштабироваться под область видимости. Правильная длина имени — это функция от того, как далеко оно путешествует и сколько контекста у читателя уже есть. В трёхстрочном цикле читатель видит всё время жизни i одним взглядом, поэтому i не просто приемлемо — более длинное имя здесь это шум, скрывающий форму цикла. На другом полюсе экспортируемая функция путешествует к местам вызова, у которых нет ничего из контекста этого файла, поэтому её имя должно нести смысл само по себе.
// Tiny scope: i and x are perfect — the whole story fits in the eye
for (let i = 0; i < points.length; i++) {
const x = points[i].x;
sum += x;
}
// Wide scope: the export must explain itself with zero local context
export function totalRevenueForFiscalQuarter(orders: Order[]): Money { /* ... */ }Правило большого пальца: короткие имена для короткоживущих локальных переменных в узкой области; длинные, раскрывающие намерение имена для всего, что пересекает границу функции, модуля или пакета. Область видимости — это ручка регулировки; длина следует за ней.
У каждого правила есть режим отказа — слепое применение делает код хуже. Согласованность и описательность — это силы, а не абсолюты, и чрезмерное применение любой из них — настоящий запах.
- Навязывание длинных имён в крошечных областях добавляет шум.
for (let currentIterationIndex = 0; ...)илиconst userObjectFromArray = points[currentIterationIndex]погребают тривиальный цикл под церемонией. Теперь читателю приходится разбирать длинные идентификаторы, чтобы восстановить структуру, которуюiиxпоказали мгновенно. - Навязывание лаконичных имён в широких областях скрывает намерение. Экспортируемая
function calc(d: Data): Rничего не говорит далёкому вызывающему — ему придётся открыть её, чтобы узнать, что она вычисляет, а это ровно та цена, которую описательные имена существуют, чтобы устранить. - Навязывание ложной согласованности связывает несвязанные вещи. Если две операции действительно различаются —
fetchUserделает сетевой вызов с ретраями, аgetUserэто чистое чтение из кэша, — схлопывание их в одно слово стирает различие, нужное читателю. Согласованность означает одно слово на одну концепцию, а не одно слово независимо от концепции.
// Over-descriptive local: ceremony obscures a 3-line loop
for (let currentIndexInLoop = 0; currentIndexInLoop < items.length; currentIndexInLoop++) { /* ... */ }
// Under-descriptive export: terse where the reader has no context
export function proc(o: Order[]): number { /* ... */ }Senior-приём — сначала прочитать область видимости, а затем выбрать длину и слово — никогда не применять «всегда будь описательным» или «всегда переиспользуй тот же глагол» механически.
Обе силы в одном модуле. До — несогласованный словарь на границе, церемониальные имена в локальных переменных:
// orders.ts
export function calc(ordersArrayParameter: Order[]): number {
let runningTotalAccumulatorVariable = 0;
for (let loopIndexCounter = 0; loopIndexCounter < ordersArrayParameter.length; loopIndexCounter++) {
const currentOrderBeingProcessed = ordersArrayParameter[loopIndexCounter];
runningTotalAccumulatorVariable += currentOrderBeingProcessed.amount;
}
return runningTotalAccumulatorVariable;
}
// elsewhere — same concept, three different verbs
const totalA = calc(await repo.fetchOrders(uid));
const totalB = calc(await svc.getOrders(uid));
const totalC = calc(await cache.loadOrders(uid));Экспорт calc слишком лаконичен для своей широкой области — вызывающий не может понять, что она суммирует. Локальные переменные (loopIndexCounter, currentOrderBeingProcessed) слишком многословны для трёхстрочного тела. А операция чтения имеет три имени (fetch/get/load), поэтому читателю приходится проверять, что это одно и то же.
После — описательно на границе, лаконично в локальных переменных, один глагол на одну концепцию:
// orders.ts
export function totalOrderAmount(orders: Order[]): number {
let total = 0;
for (const order of orders) {
total += order.amount;
}
return total;
}
// one verb for "read orders" everywhere
const totalA = totalOrderAmount(await repo.getOrders(uid));
const totalB = totalOrderAmount(await svc.getOrders(uid));
const totalC = totalOrderAmount(await cache.getOrders(uid));Экспорт теперь сообщает свой смысл вызывающему без контекста; локальные переменные цикла (total, order) короткие, потому что весь цикл помещается в поле зрения; а getOrders — единственное слово для одной концепции, поэтому читателю никогда не приходится расшифровывать синонимы. Ничего более хитрого здесь нет — оно просто сопоставляет длину каждого имени его области видимости и использует одно слово на одну концепцию.
▸Почему это работает
Почему считать несогласованный словарь связанностью, а не просто стилем? Потому что связанность — это «изменение здесь вынуждает изменить твоё понимание там». Когда User и Customer — это одно и то же под двумя именами, каждый читатель поддерживает эту эквивалентность в голове, и каждое переименование или изменение поведения приходится сверять с неписаным контрактом. Это и есть конкретность смысла: две части системы согласны насчёт договорённости, которую компилятор не видит. Свёртывание синонимов в один единый термин превращает эту связанность в голове в единственный типизированный символ, который инструменты могут отслеживать и рефакторить, — а это и есть весь смысл низкой связанности.
▸Частая ошибка
Самая частая чрезмерная коррекция — «имена всегда должны быть как можно описательнее», что порождает currentIterationIndexValue в двухстрочном цикле и theUserThatWeAreCurrentlyProcessing для времени жизни в один оператор. Описательность — это относительная к области видимости добродетель, а не абсолютная: она окупается ровно там, где у читателя нет локального контекста (экспорты, API модулей, долгоживущие поля), и становится чистым шумом там, где у него есть весь нужный контекст (плотные циклы, короткие лямбды, одноразовые временные переменные). Не измеряй имя тем, сколько оно говорит, — измеряй его тем, говорит ли оно нужное количество для того, как далеко оно путешествует.
В кодовой базе пользователь читается через getUser в сервисе, fetchUser в репозитории и retrieveUser в кэше — все три это одно и то же чтение по ключу. Почему senior-инженер называет это проблемой связанности, а не просто стилистической мелочью?
Две силы именования работают на разных масштабах. Поперёк кодовой базы используй одно слово на одну концепцию: синонимы вроде get/fetch/retrieve для одной операции или User/Customer/Account для одной сущности вынуждают каждого читателя нести неписаное соответствие «это одно и то же» — это скрытая связанность (конкретность смысла), а общий единый словарь — самый дешёвый способ её устранить. Внутри одного участка масштабируй длину имени под область видимости: лаконичные имена (i, x, total) там, где читатель видит всё время жизни, описательные имена там, где символ пересекает границу функции, модуля или пакета без локального контекста. У каждого правила есть режим отказа — длинные имена, втиснутые в крошечные области, добавляют шум, лаконичные имена, выставленные в широких областях, скрывают намерение, а ложная согласованность стирает реальные различия. Навык — сначала прочитать область видимости, а затем выбрать слово и его длину под неё.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.