Представьте объект «заказ» с тремя булевыми полями:
public class Order {
private boolean isPaid;
private boolean isShipped;
private boolean isCancelled;
}
Поначалу удобно: оплатили — ставим isPaid = true, отправили — isShipped = true. Но флаги множатся. Три булевых поля дают восемь комбинаций, и часть из них — бессмыслица. Что значит isPaid = false, isShipped = true — отправили неоплаченный заказ? А isCancelled = true, isShipped = true — отменённый, но уже уехавший? Такие состояния не должны существовать, но код их разрешает: ничто не мешает выставить любую комбинацию.
Дальше хуже. Проверки расползаются по всему коду:
if (order.isPaid() && !order.isShipped() && !order.isCancelled()) {
// можно отправлять
}
if (!order.isCancelled() && (order.isPaid() || order.isShipped())) {
// нельзя отменять? или можно? уже не помню
}
С каждым новым правилом эти if растут, дублируются и начинают противоречить друг другу. Проблема в том, что настоящего понятия «состояние заказа» в коде нет — оно размазано по флагам, и целостность держится только на дисциплине программиста. Конечные автоматы решают ровно это: делают состояние явным, а недопустимые переходы — невозможными.
Что такое конечный автомат
Конечный автомат (или стейт-машина, от англ. state machine) — это модель, в которой объект в каждый момент находится ровно в одном из заранее перечисленных состояний и переходит между ними по чётким правилам. Звучит абстрактно, но вы пользуетесь автоматами каждый день.
Возьмём турникет в метро. У него всего два состояния: «закрыт» и «открыт». В закрытом он ждёт оплаты; приложили карту — перешёл в «открыт»; прошли — вернулся в «закрыт». Толкать закрытый турникет бесполезно: он не реагирует на «толчок», только на «оплату». Это и есть суть автомата — на каждое состояние разрешён свой набор действий, остальные игнорируются.
Или светофор: зелёный → жёлтый → красный → зелёный. Порядок жёсткий, светофор не прыгает с зелёного сразу на красный.
Разберём словарь автомата на этих примерах:
- Состояние (state) — положение, в котором объект сейчас находится. У турникета — «закрыт» / «открыт». У заказа — «новый», «оплачен», «отправлен».
- Событие (event) — то, что происходит снаружи и может запустить переход: «приложили карту», «оплатили заказ», «нажали отмену».
- Переход (transition) — смена одного состояния на другое в ответ на событие. «Закрыт» + событие «оплата» → «открыт».
- Начальное состояние — то, с которого объект стартует. У заказа — «новый».
- Конечные состояния — те, из которых выхода уже нет. Заказ «доставлен» или «отменён» — дальше двигаться некуда.
- Guard (условие перехода) — дополнительная проверка, без которой переход не случится, даже если событие пришло. Например, оплатить заказ можно, только если сумма больше нуля.
- Действие при переходе (action) — что нужно сделать в момент перехода: списать деньги, отправить письмо, записать время. Действие привязано к переходу, а не разбросано по коду.
Главная идея: весь набор допустимых переходов известен заранее и собран в одном месте. Если события «отправить» нет для состояния «отменён», значит отправить отменённый заказ нельзя — и это правило записано явно, а не подразумевается.
Пример: жизненный цикл заказа
Вернёмся к заказу, но теперь опишем его как автомат. Состояния:
NEW— создан, ждёт оплаты (начальное);PAID— оплачен, готов к отправке;SHIPPED— отправлен покупателю;DELIVERED— доставлен (конечное);CANCELLED— отменён (конечное).
Разрешённые переходы:
NEW → PAID— покупатель оплатил;PAID → SHIPPED— склад отправил;SHIPPED → DELIVERED— курьер довёз;NEW → CANCELLED— отменили неоплаченный заказ;PAID → CANCELLED— отменили оплаченный (с возвратом денег).
Наглядно это выглядит так:
Жизненный цикл заказа как конечный автомат: стрелки — разрешённые переходы. Отправить можно только оплаченный заказ (NEW → PAID → SHIPPED), а отменить — лишь до отправки. Отправленный заказ отменить нельзя: такого перехода в автомате просто нет.
А теперь — что запрещено, и почему это ценно:
SHIPPED → CANCELLED— отправленный заказ отменить нельзя, товар уже в пути. Автомат просто не знает такого перехода.NEW → SHIPPED— нельзя отправить неоплаченный заказ, минуяPAID.DELIVERED → что угодно— доставленный заказ — конечное состояние, из него нет выхода.- Повторная оплата
PAID → PAID— заказ уже оплачен, второе списание невозможно.
Ценность в том, что запрет — не забытая проверка в каком-то одном if, а свойство самой модели. Если код попытается перевести SHIPPED в CANCELLED, автомат ответит ошибкой: такого перехода не существует. Ошибка вылезает сразу и в одном месте, а не превращается в тихо испорченные данные, которые всплывут через неделю в отчёте.
Флаги против стейт-машины
Сравним два подхода на одной задаче — «можно ли отправить заказ».
С флагами условие приходится собирать вручную и повторять везде, где оно нужно:
if (order.isPaid() && !order.isShipped() && !order.isCancelled()) {
order.setShipped(true);
}
Что тут плохо:
- Невозможные состояния разрешены. Три булевых поля дают восемь комбинаций, а осмысленных — меньше половины. Ничто не мешает выставить
isPaid = false, isShipped = true. - Правило дублируется. Каждый раз, когда нужно понять «в каком заказ состоянии», условие пишут заново — и легко ошибиться в одной из копий.
- Нет единого списка правил. Чтобы узнать, какие переходы вообще возможны, придётся вычитать весь код и держать логику в голове.
С автоматом заказ хранит одно поле-состояние, а правила собраны в одном месте:
public enum OrderStatus { NEW, PAID, SHIPPED, DELIVERED, CANCELLED }
public void ship() {
if (status != OrderStatus.PAID) {
throw new IllegalStateException(
"Отправить можно только оплаченный заказ, а он в статусе " + status);
}
this.status = OrderStatus.SHIPPED;
}
Что даёт этот подход:
- Состояние явное. В любой момент заказ — ровно в одном из перечисленных
enum-состояний. Невозможных комбинаций больше нет: «оплачен и отменён одновременно» просто не выразить. - Запрещённый переход = ошибка. Попытка отправить неоплаченный заказ бросает исключение сразу, а не портит данные молча.
- Единый список правил. Все допустимые переходы описаны в одном месте. Чтобы понять поведение заказа, достаточно посмотреть на таблицу состояний, а не вычитывать разрозненные
if.
Для сложных случаев проверку перехода выносят в отдельную структуру или готовый инструмент (например, Spring Statemachine или движок процессов вроде Camunda с нотацией BPMN), но идея та же: одно поле-состояние и один свод правил вместо россыпи флагов.
Когда хватит if, а когда нужен автомат
Автомат — не всегда правильный ответ. Иногда обычные if проще и честнее, и раздувать код стейт-машиной незачем. Ориентиры простые.
Хватит обычных проверок, если:
- состояний два-три и они меняются линейно, без ветвлений («черновик» → «опубликовано»);
- запрещённых переходов по сути нет — двигаться можно только вперёд;
- логика не растёт и переходы не важны сами по себе (не нужно знать, кто и когда перевёл объект).
Пора вводить автомат, если:
- состояний много и между ними сложная сеть переходов, часть которых запрещена;
- важен запрет конкретных переходов, и цена ошибки высока (деньги, отгрузка, юридические статусы);
- нужен аудит переходов — история «кто, когда и из какого состояния перевёл объект»;
- логика постоянно растёт: новые состояния и правила добавляют регулярно, а
ifуже начали противоречить друг другу.
Практический признак: если, глядя на код, вы не можете быстро ответить «какие состояния вообще бывают и какие переходы разрешены» — значит модели состояния не хватает, и её пора сделать явной. Пока же всё умещается в пару линейных шагов — не усложняйте.
Коротко
- Конечный автомат (стейт-машина) — модель, где объект всегда находится ровно в одном из перечисленных состояний и меняет их по чётким переходам в ответ на события; аналогия — турникет или светофор.
- Словарь автомата: состояние, событие, переход, начальное и конечные состояния, guard (условие перехода) и действие при переходе.
- Жизненный цикл заказа
NEW → PAID → SHIPPED → DELIVERED(иCANCELLED) явно задаёт, какие переходы разрешены, а запрещённые (SHIPPED → CANCELLED,NEW → SHIPPED) делает невозможными. - Булевы флаги разрешают невозможные комбинации, дублируют правила и прячут их по коду; автомат даёт явное состояние, ошибку на запрещённый переход и единый список правил.
- Пара линейных состояний — хватит
if; много состояний с запрещёнными переходами, важным аудитом и растущей логикой — нужен автомат.