Главная / Статьи / Семь вопросов, которые нужно ответить перед тем, как написать первую строку репортажа

Семь вопросов, которые нужно ответить перед тем, как написать первую строку репортажа

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

2797 слов

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

Почему первая реализация имеет такое большое значение

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

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

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

1. Отделите запрашиваемую функцию от основной проблемы

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

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

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

  • Кто сегодня не может выполнить свою работу?
  • Что они в настоящее время делают вручную?
  • Какое решение позволит принять эта новая информация?
  • Что становится возможным после появления этой функции?
  • Если пропустить этот шаг, инженеры естественным образом оптимизируют саму по себе заявку. Кнопка кажется простой, компонент — переиспользуемым, API — упорядоченным, но функция всё равно оставляет желать лучшего, поскольку решает конкретную задачу более точно, чем саму проблему. Речь не о том, чтобы откладывать написание кода; речь о том, чтобы убедиться, что код действительно является правильным решением.

    2. Определите, что означает успех для всей операции

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

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

    Полезным упражнением является определение границ завершения работы до написания процесса рабочего потока:

    • Эффекты, которые должны либо успешно выполниться, либо провалиться вместе, поскольку частичный результат будет недопустимым состоянием, должны находиться в одной транзакции.
  • Эффекты, которые ценны, но второстепенны, такие как письмо с приветствием, не должны определять, была ли выполнена основная операция. Обычно они должны обрабатываться в фоновом задании с возможностью повторных попыток и отдельной записью статуса «в ожидании».
  • Тот же вопрос возникает при реализации небольших функций. Когда кто-то запускает экспорт, считается ли он успешным уже при наличии файла и принятии задания, или ссылка появится позже? Когда интерфейс сообщает «Сохранено», подтвердил ли сервер сохранение данных или изменился только локальный статус?

    Если оставить всё неопределенным, каждый слой будет создавать собственное определение. Интерфейс показывает успех, в то время как бэкенд все еще работает; фоновый процесс пытается выполнить действие, которое пользователь уже считает неудачным; система мониторинга сообщает о корректном запросе, хотя важный побочный эффект исчез. Сначала определение гарантий обычно упрощает реализацию, поскольку у каждого шага теперь есть четкая задача. Это также формирует контракт ответа: API, возвращающее значение "accepted", отличается от того, что возвращает "done", и клиентам необходимо знать, какой из вариантов они получают.

    3. Определите, какой слой отвечает за каждое решение

    Функция может давать правильные результаты, но оставаться небезопасной, если решение принимается в неправильном слое. Примеры:

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

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

    Права собственности распространяются как на состояние, так и на правила. Фильтры, которые должны быть общедоступными и сохраняться после обновления, естественно подходят для использования в URL. Временные данные принадлежат форме. Разрешения и сохранённое состояние поступают с сервера, и клиент не должен создавать свою собственную версию на основе локальных предположений.

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

    4. Учет уже существующих данных

    Для желаемой модели пишется новый код. Однако производственные данные также содержат следы всех предыдущих моделей.

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

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

    • Могут ли существующие записи быть мигрированы правдиво?
    • Требуется ли временный статус «неизвестно»?
    • Приводит ли это изменение к переписыванию истории или только меняет будущее поведение?

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

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

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

    5. Исходите из того, что работа будет повторяться и будет соперничать

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

    • Пользователь кликает снова, потому что страница кажется застрявшей.
  • Соединение с мобильным интернетом прерывается после завершения работы сервера, но до прихода ответа.
  • Очередь доставляет одно и то же сообщение дважды.
  • Два администратора одобряют один и тот же пendentный элемент с небольшим интервалом во времени.
  • Запланированная задача обновляет запись, которую пользователь всё ещё просматривает в старой версии.
  • Важный вопрос, который следует задать заранее, — это безопасность выполнения операции несколько раз и её возможность одновременного выполнения. Если повторение не имеет негативных последствий, дополнительные механизмы могут оказаться ненужными. Если же повторение приводит к созданию второй оплаты, приглашения, файла или бронирования запасов, системе необходим способ распознавать, что несколько попыток представляют собой одно логическое действие.

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

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

    6. Разработайте план того, как функция будет себя проявлять в реальных условиях работы

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

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

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

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

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

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

    7. Определите, как вы докажете безопасность изменений

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

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

    Это не означает, что каждая функция требует сложного комплекса тестов типа end-to-end. Выбирайте уровень тестирования в зависимости от того, какой слой фактически обеспечивает гарантию:

    • Чистую трансформацию можно протестировать напрямую в рамках тестов на единицу.
    • Для контракта API обычно требуются тесты интеграции.
    • Инварианты, обеспечиваемые в базе данных, необходимо проверять с использованием реальной базы.
  • Рискованная миграция может потребовать мониторинга, поэтапного внедрения или использования флага функции с запланированной датой удаления.
  • Вопрос обратимости также важен. Если изменения приведут к проблемам, можно ли их отключить или откатить без потери данных, записанных за это время? Будет ли предыдущая версия приложения продолжать работать после изменения схемы, или миграция требует последовательности расширения и сжатия? Можно ли выпустить обновление для небольшой группы пользователей, пока все еще не стали на него полагаться?

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

    Сохранение соразмерности предварительных работ

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

    В упрощённой форме этот чек-лист может быть включён в описание задачи:

    • Настоящая проблема в одном предложении и кто с ней сталкивается
    • Что означает «решено» и какие эффекты являются второстепенными
    • Единственный ответственный за каждое бизнес-правило
    • План действий в отношении существующих данных и старых клиентов
    • Поведение системы при повторных попытках и при одновременном доступе
    • Что записывается в логи и как восстанавливаются сбои
  • Как будет проверяться гарантия и как можно откатить изменения
  • Заключение

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

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

    Связанные статьи

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