Головна / Статті / Сім запитань, які потрібно вирішити перед тим, як написати перший рядок статті

Сім запитань, які потрібно вирішити перед тим, як написати перший рядок статті

Чек-лист перед впровадженням, який охоплює справжню проблему користувача, гарантії завершення, власність на правила, старі дані, повторні спроби та конкуруентність, можливості моніторингу та безпеку розгортання.

2797 слів

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

Чому перша реалізація має таку велику вагу

Коли надходить запит, початок роботи з кодом перетворює абстрактне завдання на конкретне та менш масштабне. Проте відкриті питання не зникають. Хтось все одно мусить вирішити, хто відповідає за бізнес-правило, що означає „виконано“ для багатокрокової операції та що станеться з записами, створеними до змін. Якщо ніхто не приймає рішення, код приймає його випадково.

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

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

1. Відокремте запитувану функцію від основної проблеми

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

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

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

  • Хто сьогодні не може виконати свою роботу?
  • Що вони зараз роблять вручну?
  • Яке рішення допоможе ця нова інформація?
  • Що стане можливим після появи цієї функції?
  • Якщо пропустити цей крок, інженери автоматично оптимізують саме ту пропозицію, яка була подана. Кнопка зручна у використанні, компонент можна повторно використовувати, API чисте, але функція все одно розчаровує, оскільки вона вирішує конкретне завдання точніше, ніж справжню проблему. Мета не у затримці написання коду, а у переконанні, що код є правильною відповіддю.

    2. Визначте, що означає успіх для всієї операції

    Багато вимог описують певну дію, не визначаючи, коли вона завершується. Надсилання заявки, схвалення запису, копіювання проекту чи синхронізація даних здаються простими, поки не починається кілька етапів, і один з них не провалюється.

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

    Корисним завданням є побудова меж завершення перед створенням процесу роботи:

    • Ефекти, які мають успішно або невдалио виконатися разом, оскільки частковий результат буде недійсним станом, мають належати до однієї транзакції.
  • Ефекти, які є цінними, але вторинними, такі як лист-привітання, не повинні визначати, чи була успішною основна дія. Їх зазвичай слід виконувати у фоновому завданні з повторними спробами та окремим записом статусу „в очікуванні“.
  • Таке саме питання виникає при реалізації невеликих функцій. Коли хтось починає експорт, чи вважається це успішним лише тоді, коли файл створений та завдання прийняте, чи згодом з’явиться посилання? Коли інтерфейс показує „Збережено“, чи підтвердив сервер збереження даних, чи змінився лише локальний стан?

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

    3. Вирішіть, який шар відповідає за кожне рішення

    Функція може приносити правильні результати, але залишатися небезпечною, якщо рішення знаходиться у неправильному шарі. Поширені приклади:

    • Фронтенд приховує кнопку від користувачів без дозволу, але API приймає запит, коли він надсилається безпосередньо.
  • Контролер перевіряє зміну стану, але запланована задача викликає базовий сервіс та обходить цю перевірку.
  • Сервіс відхиляє дублікати, але скрипт технічного обслуговування записує дані безпосередньо в таблицю.
  • У кожному випадку правило існує, але не кожен шлях проходить через нього.

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

    Власність стосується як держави, так і правил. Фільтри, які мають бути спільнодоступними та зберігатися після оновлення, є природним елементом URL. Тимчасові дані належать до форми. Дозволи та постійний статус надходять від сервера, і клієнт не повинен створювати власну версію на основі місцевих припущень.

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

    4. Врахування вже існуючих даних

    Для бажаної моделі пишеться новий код. Однак у продуктивних даних також залишаються сліди кожної попередньої моделі.

    Зробити поле обов’язковим для заповнення легко для записів, створених після оновлення, але тисячі старіших записів можуть не мати цього поля. Перероблена модель статусів може добре описувати майбутні процеси роботи, залишаючи при цьому історичні записи у статусах, які новий код більше не визнає. Новостворений обов’язковий зв’язок може вказувати на ентитет, якого просто не існувало на момент створення старіших записів.

    Перш ніж додавати перевірки чи змінювати схему, запитайте себе:

    • Чи можна достовірно мігрувати існуючі записи?
    • Чи потрібен тимчасовий статус „невідомо“?
    • Чи ця зміна переписує історію, чи лише змінює майбутню поведінку?

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

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

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

    5. Вважайте, що робота буде повторюватися та конкурувати

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

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

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

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

    6. Сплануйте, як ця функція буде пояснювати свою роботу в умовах продакшну

    У вашому комп’ютері є точки зупинки, можливість повторення дії та свіжа інформація про структуру проекту. У умовах реальної експлуатації команда може отримати лише повідомлення про підтримку з інформацією про те, що щось не спрацювало.

    Тож уявіть розслідування перед тим, як писати код. Якщо операція зазнає невдачі, хто зможе визначити, на якому кроці виникла проблема? Чи можна відстежити одне запитання через різні сервіси? Чи покажуть журнали, чи була дія виконана один раз чи три? Чи можна розрізнити стани „ніколи не почато“, „у процесі“, „частково виконано“ та „невдача“?

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

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

    Водночас потрібно думати про відновлення. Чи безпечно перезапустити завдання, яке зазнало невдачі? Чи може служба підтримки бачити поточний стан операції без необхідності вручну запитувати кілька таблиць? Чи може користувач безпечно спробувати ще раз, і чи може хтось надати йому точну інформацію про результат?

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

    7. Вирішіть, як будете доводити безпеку змін

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

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

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

    • Чисту трансформацію можна прямо перевірити за допомогою тесту на одиницю.
    • Для контракту API зазвичай потрібен тест інтеграції.
    • Інваріант, який забезпечується в базі даних, має бути перевірений на реальній базі даних.
  • Ризикована міграція може вимагати моніторингу, поетапного впровадження або використання флага функції з запланованою датою видалення.
  • У цій же розмові має бути й питання про зворотність. Якщо зміни призведуть до проблем, чи можна їх вимкнути або скасувати, не втрачаючи даних, записаних за цей час? Чи буде попередня версія додатку продовжувати працювати після зміни схеми, чи потребує міграція послідовності розширення та скорочення? Чи можна випустити продукт для невеликої групи користувачів, перш ніж усі почнуть від нього залежати?

    Якщо дизайн важко тестувати або складно змінити, це часто є ознакою того, що певна операція несе занадто багато обов’язків або що для змін потрібен менший проміжний крок. Абсолютних доказів досягти неможливо, адже у програмного забезпечення завжди є елемент невизначеності. Важливо, щоб критичні гарантії були видимі у тестах та показниках, а небезпечні рішення можна було скасувати, щоб помилка стала уроком, а не постійними наслідками.

    Збереження пропорційності попередньої роботи

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

    У спрощеній формі цей чек-лист може бути включений у опис тикета:

    • Справжня проблема в одному реченні та хто її має
    • Що означає „виконано“ та які наслідки є вторинними
    • Єдиний власник кожного бізнес-правила
    • План щодо існуючих даних та старих клієнтів
    • Поведінка під час повторної спроби та при одночасному доступі
    • Що фіксується у журналі та як відновлюються збої
  • Як буде перевірятися гарантія та як можна скасувати зміни
  • Підсумок

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

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

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

  • Проектування бекенд-систем за принципом вузького місця: від скорочувачів URL до електронної комерції — підхід, який спочатку враховує вимоги для проектування бекендів на Node.js: коли варто додавати балансувальники навантаження, Redis, репліки, черги та обмеження швидкості, та скільки кожен з цих елементів коштує.