Состояния и переходы до разработки
Практический материал для аналитика и тимлида: как собрать модель состояний, таблицу переходов и запрещенные сценарии до разработки, тестирования и интеграции.
Когда использовать
Модель состояний нужна, когда у объекта или операции есть жизненный цикл, а команда по-разному понимает:
- что означает текущий статус;
- кто и каким событием может его изменить;
- какие переходы допустимы;
- что считается финальным результатом;
- можно ли вернуться назад;
- что происходит после таймаута, ошибки или отмены;
- какие состояния видят пользователь, оператор и внешняя система;
- где нужен ручной разбор.
Это бывает в заявках, заказах, платежах, документах, согласованиях, фоновых операциях и внешних интеграциях.
Статус - не подпись на экране
Статус влияет на поведение системы:
- какие действия доступны;
- можно ли редактировать данные;
- допустим ли повтор;
- кто отвечает за следующий шаг;
- какие уведомления отправляются;
- когда запускается таймер;
- что попадает в мониторинг;
- какой результат увидит другой участник.
Если статус меняет хотя бы одно правило, его нужно описывать как состояние с явными переходами, а не как свободный текст.
Начать с объекта и границы
До списка статусов ответьте:
Чей жизненный цикл описываем?
Где хранится его текущее состояние?
Кто считает это состояние источником истины?
Какие соседние объекты имеют отдельный жизненный цикл?
Например, статус пользовательской заявки и статус отправки во внешний контур - не одно и то же. Заявка может оставаться “на проверке”, пока техническая операция переживает таймаут, повтор или ручной разбор. Если смешать два объекта в одном поле, продуктовый смысл начинает зависеть от транспортной ошибки.
Шаг 1. Собрать состояния из рабочего языка
Источники состояний:
- действия пользователя и оператора;
- ответы внешней системы;
- правила редактирования;
- ошибки и таймауты;
- ручные обходы;
- уведомления;
- отчеты и фильтры;
- вопросы поддержки “где сейчас заявка”;
- фразы вроде “уже нельзя изменить” или “нужно дождаться”.
Для каждого кандидата спросите:
- поведение системы действительно меняется;
- есть отдельный владелец следующего действия;
- состояние нужно хранить и восстанавливать;
- пользователь или поддержка должны его различать;
- по нему принимается решение о повторе, отмене или завершении.
Если ничего не меняется, возможно, это не состояние, а причина, отметка, атрибут или текст сообщения.
Шаг 2. Отделить состояния от событий
Состояние отвечает на вопрос “где объект сейчас”. Событие отвечает на вопрос “что произошло”.
| Состояние | Возможное событие |
|---|---|
draft |
Пользователь отправил заявку |
checking |
Проверка нашла недостающие данные |
needs_changes |
Пользователь исправил данные |
approved |
Проверка завершилась успешно |
rejected |
Найдено основание для отказа |
cancelled |
Пользователь или оператор отменил процесс |
Фразы sent, timeout и retry могут быть как событиями, так и состояниями -
это зависит от модели. Важно договориться, что хранится, а что только вызывает
переход.
Шаг 3. Нарисовать только разрешенный жизненный цикл
stateDiagram-v2
direction TB
state "Черновик" as draft
state "Проверка" as checking
state "Одобрено" as approved
state "Отказ" as rejected
[*] --> draft
draft --> checking: отправили
checking --> approved: условия выполнены
checking --> rejected: есть основание
approved --> [*]
rejected --> [*]
Для первой схемы достаточно основного финального контура. Возврат на доработку, отмену, таймауты и другие ветки стоит добавлять только тогда, когда они остаются читаемыми и помогают обсуждению.
Диаграмма показывает форму жизненного цикла, но не заменяет спецификацию. Условия, владельцы, побочные действия и ошибки удобнее проверять в таблице.
Шаг 4. Собрать таблицу переходов
Минимальные колонки:
| Было | Событие | Условие | Стало | Кто меняет | Побочное действие |
|---|---|---|---|---|---|
draft |
Заявку отправили | Обязательные поля заполнены | checking |
Пользователь / система | Зафиксировать версию данных |
checking |
Не хватает данных | Разрешена доработка | needs_changes |
Проверяющий | Отправить понятный список замечаний |
needs_changes |
Данные исправили | Проверка формата пройдена | checking |
Пользователь / система | Создать новую попытку проверки |
checking |
Условия выполнены | Нет блокирующих причин | approved |
Проверяющий / правило | Зафиксировать решение и уведомить |
checking |
Найдено основание | Код причины известен | rejected |
Проверяющий / правило | Сохранить безопасное объяснение |
draft |
Процесс отменили | Обработка еще не началась | cancelled |
Пользователь | Не запускать проверку |
Для интеграции добавьте:
- внутренний и внешний идентификатор;
- idempotency key;
- число попыток;
- время последнего события;
- источник изменения;
- correlation id;
- признак ручного действия.
Эти поля не обязаны жить в одной таблице базы. Они должны быть видимы в аналитической модели и контракте.
Шаг 5. Явно перечислить запрещенные переходы
Разрешенная схема не всегда показывает, что именно команда должна защищать. Отдельно выпишите опасные переходы:
| Запрещенный переход | Почему опасен | Как защищаем |
|---|---|---|
draft -> approved |
Проверка пропущена | Разрешать одобрение только из checking |
rejected -> checking |
Финальное решение переписано без нового основания | Создавать пересмотр или новую версию отдельно |
cancelled -> approved |
Отмененный процесс оживает после позднего ответа | Игнорировать поздний ответ и записывать диагностическое событие |
approved -> needs_changes |
Финальный результат становится неясным | Использовать отдельный процесс отзыва или корректировки |
| Любое состояние -> неизвестный статус | Потребитель не понимает новое значение | Валидировать контракт и определить поведение совместимости |
Запрещенный переход стоит превратить в acceptance criterion или автоматическую проверку, если его цена высока.
Шаг 6. Разобрать таймаут, повтор и поздний ответ
Таймаут не означает, что действие не произошло. Команда должна решить:
- остается ли объект в ожидании;
- появляется ли отдельное техническое состояние;
- можно ли проверить результат по идентификатору;
- допустим ли автоматический повтор;
- как повтор защищен от дубля;
- что делать с ответом, который пришел после отмены или ручного решения;
- когда процесс переходит к оператору;
- кто получает сигнал о зависшем состоянии.
Для дорогих операций полезно разделять:
- продуктовый статус объекта;
- статус технической попытки;
- решение о повторе;
- ручное действие.
Так пользовательский результат не меняется напрямую из-за сетевой детали.
Шаг 7. Определить финальность
Финальное состояние не всегда означает “никогда не изменится”. Оно означает, что текущий процесс завершен и продолжение требует отдельного правила.
Для каждого финального состояния зафиксируйте:
- можно ли начать пересмотр;
- создается новая версия или меняется старая;
- что видят внешние потребители;
- какие уведомления уже отправлены;
- можно ли компенсировать результат;
- какие данные остаются доступными для аудита;
- кто имеет право запустить следующий процесс.
Не стоит возвращать финальный объект в середину старого жизненного цикла без явного решения. Это усложняет аудит и делает историю неоднозначной.
Шаг 8. Связать переходы с приемкой
Для важного перехода нужен проверяемый сценарий:
Дано:
заявка находится в состоянии checking.
Когда:
внешняя проверка не отвечает до таймаута.
Тогда:
- заявка не становится approved или rejected;
- техническая попытка получает диагностируемый результат;
- повтор не создает вторую операцию;
- поддержка видит correlation id и время попытки;
- пользователь получает безопасное сообщение без технических деталей.
Проверьте как минимум:
- основной успешный путь;
- предметный отказ;
- отмену;
- нехватку данных;
- таймаут;
- повтор одного события;
- события в неправильном порядке;
- поздний ответ;
- попытку перейти из финального состояния;
- неизвестное значение внешнего статуса.
Шаг 9. Назначить владельца перехода
У перехода должен быть источник:
- пользователь;
- оператор;
- бизнес-правило;
- фоновая задача;
- внешний контур;
- таймер;
- администратор;
- процесс компенсации.
Фраза “система переводит” слишком общая, если непонятно, какой компонент проверяет условие и сохраняет результат. Для спорных переходов полезно зафиксировать владельца в API-контракте или записи решения.
Короткий шаблон
Объект:
Источник истины:
Состояния:
- название;
- смысл;
- кто видит;
- доступные действия;
- финальное или промежуточное.
Переходы:
- было;
- событие;
- условие;
- стало;
- владелец;
- побочные действия.
Запрещенные переходы:
- ...
Ошибки и ожидание:
- таймаут;
- повтор;
- поздний ответ;
- ручной разбор.
Приемка:
- основной путь;
- альтернативы;
- неправильный порядок;
- повтор;
- финальность.
Открытые вопросы:
- ...
Антипаттерны
- один статус одновременно описывает бизнес-результат и техническую попытку;
- список статусов есть, но переходы не определены;
- переход может выполнить “система”, но компонент и условие неизвестны;
errorскрывает предметный отказ, таймаут и ручной разбор;- финальные состояния меняются как обычные;
- поздний ответ переписывает отмененное или подтвержденное решение;
- новый статус внешнего API молча считается успешным;
- диаграмма есть, но QA и поддержка не могут получить из нее сценарии;
- в логах хранится больше данных, чем нужно для диагностики;
- модель пытается описать все будущие состояния до проверки первого контура.
Куда идти дальше
- Если пока непонятно, какой артефакт нужен, используйте карту выбора диаграммы.
- Если важен порядок взаимодействий, соберите sequence-диаграмму общего сценария.
- Для внешнего API свяжите переходы с контрактом ошибок и идемпотентностью.
- Существенный trade-off по статусам, повторам и ручному разбору сохраните в записи решения.
Модель состояний нужна не ради полного каталога статусов. Она делает видимыми правила, которые иначе обнаруживаются только в коде, тестах или инциденте.