Skip to content

Latest commit

 

History

History
295 lines (206 loc) · 28.8 KB

File metadata and controls

295 lines (206 loc) · 28.8 KB

Имена

После того, как мы «сдули с кода пыль», мы готовы рассматривать более серьёзные проблемы.

Изменения и техники, о которых мы будем говорить дальше, значительно труднее ранжировать от простых к сложным. Не всегда очевидно, когда и какое изменение необходимо применить следующим.

Порядок глав 🎯
Я предлагаю читателям воспринимать эту и следующие главы не как «пошаговый мануал», а скорее как справочник со списком проблем и их возможных решений.
Главы в книге расположены так, что размер предлагаемых изменений постепенно растёт. Но нет гарантий, что при рефакторинге реального проекта проблемы будут встречаться именно в таком порядке.
Будьте внимательны при анализе кода. Подмечайте и выписывайте проблемы так, как вам удобнее всего, а эту книгу используйте как вспомогательный материал.

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

Общие рекомендации

За «непонятным» именем могут скрываться ценные детали проекта и опыт прошлых разработчиков. Нам важно сохранить их, поэтому стоит погрузиться в смысл «непонятного имени». Если во время рефакторинга мы видим имя, смысл которого нам не ясен, то можно попробовать:

  • найти кого-то в команде, кто знает значение этого имени;
  • найти документацию к коду, чтобы узнать смысл оттуда;
  • в крайнем случае провести серию экспериментов, меняя значение переменной, и смотря, как это влияет на работу кода.
К слову 🔬
Если влияние переменной можно засечь на «шве», то с экспериментами помогут тесты. Их результаты покажут, как меняется работа кода в зависимости от различных значений этой переменной, что поможет сделать предположение о её сути.
В остальных случаях оценивать влияние на работу кода, вероятно, придётся вручную. Один из вариантов оценки — понаблюдать в отладчике, как изменения отражаются на других переменных и данных, с которыми работает код.
Такие эксперименты не дадут гарантий, что мы поймём смысл сущности правильно,1 но смогут подсказать, каких знаний о проекте нам не хватает.

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

Слишком короткие имена

Имя — хорошее, если оно адекватно отображает информацию о предметной области или смысле переменной. Чрезмерно короткие имена и необщепринятые сокращения этого не делают. Рано или поздно такое имя будет прочитано неправильно, потому что все «знающие» разработчики перестанут работать над проектом.

// Имена `d`, `c` и `p` слишком короткие,
// чтобы сделать вывод о смысле переменных:

let d = 0;
if (c === "HAPPY_FRIDAY") d = p * 0.2;

// Если мы назовём переменные полными словами,
// то понять смысл операции станет проще:

let discount = 0;
if (coupon === "HAPPY_FRIDAY") discount = price * 0.2;

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

То же относится к сокращениям. Я предпочитаю не использовать сокращения в коде, кроме 2 случаев:

  • если сокращение общеизвестное (USA, OOP, USD);
  • если оно используется в предметной области именно в таком сокращённом виде (Challenge Rate как CR в калькуляторе монстров для D&D).

В остальных случаях я предпочитаю писать имя для переменной в полном виде:

// Всё в порядке, сокращение USD общеизвестное:
const usd = {};

// Может быть приемлемо, если предметная область программы
// связана с математикой, а модуль работает с производными:
const dx = 0.42;

// Не лучший вариант, лучше расшифровать:
const ec = 0.6188;

// Хорошо, так понятнее:
const elasticityCoefficient = 0.6188;

Слишком длинные имена

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

Когда функциональность слабо связана по смыслу, имя старается передать весь контекст работы в одной фразе. Это раздувает имя, делает его шумным. Один из сигналов обратить внимание на имя — это наличие в нём слов типа that, which, after и т.д.

Чаще всего длинными именами «болеют» функции, которые делают слишком много. Такие функции пытаются объясняться терминами, которые для них либо слишком примитивны, либо наоборот — слишком абстрактны, и им приходится искать подходящие слова. Моя главная эвристика для поиска таких функций такова: если я читаю код функции и не могу придумать имя покороче, скорее всего, она делает слишком много.

async function submitOrderCreationFormIfValid() {
  // ...
}

Функция submitOrderCreationFormIfValid из примера выше делает сразу 3 вещи:

  • обрабатывает отправку формы;
  • валидирует данные из неё;
  • создаёт заказ.

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

// Сериализует форму в объект
// или другую удобную для работы структуру:
function serializeForm() {}

// Валидирует данные из формы:
function validateFormData() {}

// Создаёт объект заказа по собранным данным:
function createOrder() {}

// Отправляет заказ на сервер:
async function sendOrder() {}

// Реагирует на DOM-событие, вызывая другие функции:
function handleOrderSubmit() {}

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

async function handleOrderSubmit(event) {
  const formData = serializeForm(event.target);
  const validData = validateFormData(formData);
  const order = createOrder(validData);
  await sendOrder(order);
}

Кажется, что мы так ухудшаем точность имени корневой функции, ведь мы убираем из него детали. Но всё дело в том, какие именно детали мы убираем.

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

- Обработать    handle +
- отправку      submit +
- формы заказа  order
------------------------
- handleOrderSubmit

А вот как именно это будет происходить, важно уже самой функции-обработчику handleOrderSubmit. Детали реализации важны только внутри функции, но не важны в её имени. Внутри же эти детали можно выразить через имена внутренних функций.

К слову 👀
Вероятно, вы узнали в этом пример разделения уровней абстракции.2 Подробнее об этом мы поговорим в отдельной главе.

Основное решение проблем слишком длинных имён — посмотреть, что именно имя пытается рассказать. Мы можем постараться вытащить из имени все детали, которые оно несёт, а затем разделить их на «важные снаружи» и «важные внутри». Это помогает разделить задачу на более простые.

При этом 🛠
Логика функции сама по себе вполне может быть сложной. Просто если её «внутренности» собраны в «кучки по смыслу», проблем с именованием, как правило, возникает меньше.

Вариант именования функций

Если говорить о функциях, то лавировать между «слишком короткими» и «слишком длинными» именами помогает шаблон A/HC/LC.3 Он предлагает сочетать в себе само действие, кто его совершает или над чем его совершают:

prefix? + action (A) + high context (HC) + low context? (LC)

Его можно использовать как опорную или стартовую точку при именовании функций, изменяя имя по мере необходимости.

Одинаковые имена у разных сущностей

Во время рефакторинга также стоит обратить внимание на ощущение «путаницы» в коде. Оно может возникнуть, когда разные явления или предметы в коде описаны одинаковыми именами.

При чтении мы в первую очередь опираемся на имена классов, функций и переменных, чтобы понять смысл кода. Если имя не соотносится с переменной 1 к 1, нам приходится каждый раз вспоминать, о какой именно переменной идёт речь. Для этого нам нужно держать в голове «мета-информацию» об этой переменной и каждый раз обращаться к ней. Это делает чтение сложнее.

К слову 🎫
В некоторых случаях это же относится и к разным именам для одной переменной. Например, лучше избегать разных имён для доменных сущностей, но об этом чуть позже.

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

// Здесь `user` — это объект с данными пользователя:
function isOldEnough(user, minAge) {
  return user.age >= minAge;
}

// А тут — имя:
function findUser(user, users) {
  return users.find(({ name }) => name === user);
}

// С первого взгляда их сложно различить,
// потому что имена «намекают»,
// будто переменные значат одно и то же.

// Это может вводить в заблуждение; нам придётся помнить,
// что внутри функции `findUser` переменная `user`
// относится не к объекту пользователя, а к имени.

// Лучше выразить смысл переменной точнее прямо через имя:
function findUser(userName, users) {
  return users.find(({ name }) => name === userName);
}

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

Однако 🦦
Из этого правила, конечно, есть исключения. Например, одно и то же имя может использоваться для разных целей, если код страхует система типов, или если из контекста использования понятно, о чём именно идёт речь. Но по умолчанию лучше использовать разные имена.

Повсеместный язык

Бороться с проблемой именования помогает повсеместный язык (Ubiquitous Language). Это набор терминов, описывающих предметную область, которыми пользуется вся команда. Под «всей командой» мы имеем в виду не только команду разработки, а всего продукта в целом, включая дизайнеров, продукт-оунеров, стейкхолдеров и т.д.

К слову 🎙
Сам термин пришёл из методологии предметно-ориентированного проектирования (Domain-Driven Design, DDD).4 Мне она кажется удобной для описания предметной области, возможно, вам она тоже окажется полезной.

На практике это значит, что если «люди из бизнеса» для описания заказов используют термин «Заказ» (Order), то именно этим словом мы и будем называть заказы в коде, тестах, документации и устной коммуникации.

Сила повсеместного языка в однозначности. Если все называют вещь одним и тем же именем, то потерь при «переводе с языка бизнеса на язык разработки» будет меньше.

Подробнее 📚
Подробнее об этом писал Скотт Влашин в книге “Domain Modelling Made Functional”, очень рекомендую к прочтению.5

Когда мы выражаемся понятиями, близкими к реальности, ментальная модель программы становится ближе к тому, что мы моделируем. Неправильное поведение программы в этом случае заметить гораздо проще.

Врущие имена

Иногда во время рефакторинга находятся имена, которые неадекватно отражают суть переменной или функции. Они могут быть очень неточными или вовсе врать. Такие имена лучше исправить как можно раньше.

Если мы не уверены, как будет правильнее назвать переменную, то стоит выписать причины, почему имя не подходит. Например, если мы встретили вот такой код:

const trend = currentValue - previousValue;

// «Тренд» описан как разница между «Текущим» и «Предыдущим»
// значением некоторой характеристики.

...А в разговорах мы замечали, что в команде это называют «Дельтой» (а не «Трендом»), то стоит выписать это замечание в список. Получится что-то типа:

  • «Тренд», вероятно, описывает суть не точно;
  • Устно чаще используется термин «Дельта», в том числе — продукт-оунерами;
  • Специфика проекта, возможно, требует, чтобы тренд строился по более, чем двум точкам.

Эти опасения можно представить команде, чтобы подумать над переименованием.

Если имя очевидно врёт, то этап обсуждения можно пропустить. Но если есть сомнения «Переименовывать ли?», то стоит сперва их обсудить с другими разработчиками и продукт-оунером.

Типы для описания домена

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

Однако 💬
Можно поспорить, сказав, что тип переменной видно только в сигнатурах или при наведении на неё, а имя видно всегда.
Но, во-первых, большое количество длинных названий будет шуметь, и ценность деталей исчезнет. А, во-вторых, в тип я обычно выношу детали, которые не нужны мгновенно — за такими деталями можно обратиться к подсказке в IDE.
Мне кажется, что такой компромисс вполне допустим.

К примеру, типами удобно описывать состояния, которые проходят данные на разных этапах работы приложения:

type CreatedOrder = {
  createdAt: TimeStamp;
  createdBy: UserId;
  products: ProductList;
};

type ProcessedOrder = {
  createdAt: TimeStamp;
  createdBy: UserId;
  products: ProductList;
  address: Delivery;
};

Мы таким образом не только описываем разные состояния данных (по сути разные сущности) отличающимися именами, но и запрещаем использовать «неправильный» тип там, где он не подходит. Например, можно запретить попытки отправить неподготовленные заказы:

function sendOrder(order: ProcessedOrder) {
  // ...
}

const order: CreatedOrder = {
  /*...*/
};

// Вызов функции ниже не скомпилируется,
// потому что тип аргумента не подходит под сигнатуру функции.
// Такую сигнатуру можно воспринимать как предупреждение:
// «заказ ещё не готов к тому, чтобы пытаться его отправить».
sendOrder(order);
Подробнее 🚩
О том, «почему просто не использовать Boolean-флаги для разных состояний», мы поговорим подробнее в главе о статической типизации.

Footnotes

  1. «После» не значит «вследствие», Википедия, https://ru.wikipedia.org/wiki/Логическая_ошибка#Мнимая_логическая_связь

  2. Уровни абстракции, Википедия, https://ru.wikipedia.org/wiki/Уровень_абстракции_(программирование)

  3. Шаблон именования A/HC/LC, https://github.com/kettanaito/naming-cheatsheet#ahclc-pattern

  4. “Domain-Driven Design” by Eric Evans, https://www.goodreads.com/book/show/179133.Domain_Driven_Design

  5. “Domain Modeling Made Functional” by Scott Wlaschin, https://www.goodreads.com/book/show/34921689-domain-modeling-made-functional