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

Согласованность и область видимости

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

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

Ты открываешь сервис и читаешь getUser, через три файла — fetchUser, потом retrieveUserById, потом loadCustomer. Это четыре операции над четырьмя концепциями или одна операция над одной концепцией, записанная четырьмя способами? По именам не понять — поэтому ты идёшь читать все четыре реализации, чтобы выяснить, что это одно и то же. Этот налог на чтение платит каждый, кто когда-либо тронет этот код, — всегда.

Имя — это не просто ярлык на одном символе. Это обещание о том, как этот символ связан со всеми остальными именами в системе. Когда словарь несогласован, читателю приходится выучить, что fetchX и getX значат одно и то же, — и это выученное соответствие есть связанность, живущая у него в голове, а не в коде.

Цель

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

1

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

2

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

3

Длина имени должна масштабироваться под область видимости. Правильная длина имени — это функция от того, как далеко оно путешествует и сколько контекста у читателя уже есть. В трёхстрочном цикле читатель видит всё время жизни 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 { /* ... */ }

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

4

У каждого правила есть режим отказа — слепое применение делает код хуже. Согласованность и описательность — это силы, а не абсолюты, и чрезмерное применение любой из них — настоящий запах.

  • Навязывание длинных имён в крошечных областях добавляет шум. 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-уровень. Открой, попробуй, потом открой ответ.

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

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

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

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

Trademarks belong to their respective owners. Editorial reference only.