Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Конечные автоматы в бизнес-логике

Конечные автоматы в бизнес-логике: состояния вместо набора условий

Конечные автоматы: явная таблица переходов вместо рассеянных условий, защита от недопустимых статусов, хранимое состояние и тестирование переходов.

Прямой ответ

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

Типичная реализация без автомата:

if order.status == "new" and payment_ok:
    order.status = "paid"
elif order.status == "paid" and shipped:
    order.status = "shipped"
# ещё десять условий в других модулях

Проблема не в синтаксисе, а в распределении правил. Условие перехода в одном модуле может противоречить условию в другом, и проверка требует чтения всего кода.

Автомат задаёт переходы в одном месте:

TRANSITIONS = {
    ("new", "pay"):        "paid",
    ("paid", "ship"):      "shipped",
    ("shipped", "deliver"):"delivered",
    ("new", "cancel"):     "cancelled",
    ("paid", "refund"):    "refunded",
}

def apply_event(state: str, event: str) -> str:
    try:
        return TRANSITIONS[(state, event)]
    except KeyError:
        raise InvalidTransition(f"{state} + {event}")

Три свойства такого подхода:

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

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

3. Изменение локализовано. Добавление нового перехода — изменение таблицы, а не поиск мест, где проверяется статус.

Состояние хранится в базе как отдельное поле, а переход выполняется в транзакции:

UPDATE orders
   SET status = 'paid', updated_at = now()
 WHERE id = 1001 AND status = 'new';

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

Альтернативный подход — выводить состояние из истории событий, а не хранить его; о механизме — в статье про event sourcing.

Практика: применение автомата состояний

1. Описывайте состояния перечислением. Строковые литералы в разных модулях расходятся в написании, а перечисление проверяется при импорте.

2. Разделяйте состояние и событие. Состояние — где находится сущность, событие — что произошло. Смешение приводит к состояниям вроде «ожидает_отгрузки_после_оплаты».

3. Задайте таблицу переходов явно. Словарь пар «состояние + событие» читается целиком и проверяется тестами на полноту.

4. Отклоняйте недопустимые переходы ошибкой. Молчаливое игнорирование скрывает ошибку вызывающего кода.

5. Проверяйте текущее состояние в запросе обновления. Условие по статусу защищает от гонки при одновременных переходах.

6. Фиксируйте переход в журнале. Запись о том, кто и когда выполнил переход, необходима для разбора; о переходах заказов — в статье про статусы заказа.

7. Тестируйте недопустимые переходы. Тест, ожидающий ошибку при недопустимом переходе, важнее теста успешного перехода.

8. Не добавляйте состояния без необходимости. Каждое новое состояние увеличивает таблицу переходов нелинейно: для пяти состояний и пяти событий возможных пар двадцать пять.

Рекомендации:

  1. Начинайте с перечня состояний до написания кода. Обсуждение допустимых переходов с продуктом дешевле исправления логики после релиза.
  2. Различайте терминальные состояния. Состояние, из которого нет выходов, требует отдельной обработки: из него нельзя перейти, и попытки должны отклоняться явно.
  3. Учитывайте откат перехода. Если переход отменяем, обратный переход описывается в таблице, а не выполняется сбросом состояния.
  4. Проверяйте полноту переходов. Состояние, из которого нет ни одного выхода и которое не является терминальным, указывает на пробел в логике.
  5. Храните историю переходов. Текущее состояние не показывает путь, а путь часто важен для разбора.

Практический сценарий: недопустимый статус из-за распределённых проверок

Контекст: сервис заказов работал три года. Периодически в базе появлялись заказы в состоянии «отгружен» без записи об оплате.

  1. Диагностика. Проверки статусов находились в четырёх модулях: оформление, оплата, отгрузка и интеграция с внешним складом. Интеграция со складом обновляла статус напрямую, не проверяя оплату, поскольку исходила из того, что заказ не может попасть к ней неоплаченным.
  2. Причина. Правила переходов не были собраны в одном месте. Каждый модуль проверял свою часть, а предположение о порядке шагов не было зафиксировано в коде.
  3. Действия. (а) Состояния вынесли в перечисление, а переходы — в единую таблицу; (б) обновление статуса перевели на функцию применения события, которая отклоняет недопустимые переходы; (в) запрос обновления дополнили условием по текущему состоянию; (г) добавили тесты на все недопустимые пары; (д) существующие некорректные заказы разобрали вручную и добавили метрику на число записей в недопустимых сочетаниях.
  4. Результат. Недопустимые переходы отклоняются на уровне функции, а не зависят от дисциплины в каждом модуле. Метрика показывает ноль таких записей за квартал.

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

FAQ

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

Где хранить состояние? В базе как отдельное поле с обновлением в транзакции. Состояние в памяти процесса теряется при перезапуске.

Как обрабатывать одновременные переходы? Условием по текущему состоянию в запросе обновления. Если строка не обновлена, состояние изменилось одновременно, и переход нужно перечитать.

Чем автомат отличается от набора условий? Правила собраны в одном месте и проверяются на полноту. Недопустимый переход отклоняется явно, а не допускается по ошибке.

Источники: теория конечных автоматов, Martin Fowler (Domain Events), Python docs (enum) — ссылки в шапке.