Главная / Статьи / Девять привычек на уровне кода, которые делают работу старших инженеров более надежной

Девять привычек на уровне кода, которые делают работу старших инженеров более надежной

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

1471 слов

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

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

Есть девять конкретных привычек, которые встречаются настолько часто, что их стоит целенаправленно осваивать.

1. Предохранительные конструкции вместо пирамиды апокалипсиса

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

function processWithdrawal(account, amount) {
  if (account.isActive) {
    if (amount > 0) {
      if (account.balance >= amount) {
        return debit(account, amount);
      }
    }
  }
  throw new Error("Withdrawal failed");
}

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

function processWithdrawal(account, amount) {
  if (!account.isActive) throw new AccountInactiveError();
  if (amount <= 0) throw new InvalidAmountError();
  if (account.balance < amount) throw new InsufficientFundsError();

  return debit(account, amount);
}

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

2. Названия, описывающие бизнес-логику, а не тип данных

Называние переменных в соответствии с их формой, а не значением — data, res, obj, list — кажется разумным в одном файле. Однако при наличии сорока файлов, в каждом из которых data означает что-то другое, это становится крайне неудобным.

data = fetch(order_id)
if data["status"] == "done":
    process(data)

По сравнению с:

order = fetch_order(order_id)
if order.is_fulfilled:
    archive_order(order)

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

3. Единый интерфейс между вашим кодом и внешним миром

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

// scattered everywhere
const price = apiResponse.line_items[0].unit_price_cents / 100;

Лучшим подходом является преобразование ответа ровно один раз, непосредственно на границе:

function toLineItem(raw) {
  return {
    label: raw.description,
    priceInDollars: raw.unit_price_cents / 100,
  };
}

Если поставщик позже переименует unit_price_cents в price, нужно изменить ровно одну функцию. Все последующие элементы останутся нетронутыми и вообще не заметят разницы.

4. Модели данных, которые не могут лгать

Тип, состоящий из десятка факультативных полей, — это тип, который перестал стремиться точно отражать реальность:

type Ticket = {
  id?: string;
  assignee?: string;
  resolvedAt?: Date;
  resolution?: string;
};

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

type OpenTicket = { id: string; assignee?: string };
type ResolvedTicket = { id: string; assignee: string; resolvedAt: Date; resolution: string };

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

5. Разделение вопроса и команды

Когда бизнес-правила и побочные эффекты переплетаются, тестирование становится сложным — а когда тестирование становится сложным, правила вообще перестают тестироваться:

def promote_employee(employee_id):
    emp = get_employee(employee_id)
    if emp.tenure_months < 12:
        raise Error("Not eligible yet")
    if emp.current_rating < 3:
        raise Error("Rating too low")
    give_raise(emp)
    notify_hr(emp)
    log_promotion(emp)

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

def promotion_eligibility(emp):
    if emp.tenure_months < 12:
        return Ineligible("Not enough tenure")
    if emp.current_rating < 3:
        return Ineligible("Rating too low")
    return Eligible()

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

6. Комментарии должны объяснять почему, а не что

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

// increment the counter
counter++;

Комментарий, отражающий логику принятого решения, стоит сохранить:

// Retry once — the vendor's webhook occasionally arrives before
// the payment record finishes committing on their end.
retryOnce(processWebhook, payload);

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

7. Ошибки, указывающие на полезную информацию

Сообщение об ошибке вроде "Неверный ввод" не даёт следующему разработчику никаких указаний. Что именно неверно? Какой ввод? Полезное сообщение об ошибке содержит достаточно деталей, чтобы человек мог принять соответствующие действия:

{
  "code": "INVALID_DATE_RANGE",
  "message": "End date must be after start date.",
  "field": "endDate"
}

Нередко встречается код фронтенда, который анализирует текст ошибок, чтобы определить, какое сообщение отобразить — что-то вроде if (err.message.includes("date")). Такая структура по своей сути уязвима. Как только кто-то изменяет формулировку сообщения на бэкенде, логика интерфейса молча перестаёт работать. Код существует для того, чтобы машины могли на его основе принимать решения; сообщения существуют для того, чтобы их могли читать люди. Разделение этих двух аспектов позволяет избежать ситуаций, когда один вынужден неуклюже замещать другой.

8. Один pull request, одна идея

Pull request с названием вроде "исправить проблемы с оплатой", охватывающий десяток файлов и содержащий шесть не связанных между собой изменений, практически невозможен к рассмотрению. Тот, кто его проверяет, либо одобряет его без тщательной проверки, либо тратит час на попытку выяснить, какое изменение привело к тому или иному эффекту.

Дисциплинированный подход может показаться чрезмерно осторожным: переименовать поле в одном PR, ввести новую проверку во втором и подключить её к работе системы в третьем. Такой способ работы кажется более медленным в момент реализации. Однако он значительно ускоряет процесс ревью, и когда позже что-то ломается в продакшене, git log дает чёткий ответ вместо необходимости изучения 400-строчного отчёта о различиях.

9. Считайте первый черновик всего лишь черновиком

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

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

Общая идея

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

Связанные материалы

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