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

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

Розглядає дев’ять конкретних практик програмування — від захисних клоз у до суворого моделювання даних — які роблять код більш стійким, зрозумілим та легшим для виправлення помилок під тиском.

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. Сприймайте перший чернеток як чернетку

Ця остання звичка менше пов’язана з самим кодом, а більше — з его. Розробники на початку кар’єри часто сприймають першу версію, яка працює, як готовий продукт — вона функціонує, тож її випускають. Більш досвідчені інженери пишуть цю першу версію, вже припускаючи, що перечитають її з критичним поглядом перед тим, як вона потрапить у продакшен.

Саме під час цього другого проходження вкладені умовні оператори перетворюються на прості клозули, нечіткі назви отримують нові імена, а так званий „неможливий“ стан виявляється ще до того, як клієнт зіткнеться з ним. Це незначна звичка — зупинитися, перечитати та запитати себе, чи не спричинить це плутанину у того, хто не має жодного контексту, — але саме вона дозволяє іншим восьми звичкам справді застосовуватися на практиці.

Спільна нитка

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

Пов’язана література

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