State Machine и Хореография

State Machine и Хореография

В прошлых постах мы разобрали, что такое хореография и как ее реализовывать. А теперь соединим хореографию с паттерном State Machine. Это позволит построить нам прозрачную и управляемую архитектуру.

При использовании хореографии логику взаимодействия между частями системы координируют сами участники взаимодействия. Но у каждого участника при этом свой жизненный цикл бизнес-логики/бизнес-домена. Если событий, на которые должен реагировать участник хореографии, становится больше 2-3, то логика обработки событий становится очень запутанной. В этом случае нам поможет использование State Machine для реакции на события. Это позволит согласовать внутреннее состояние сервиса с событиями в системе.

Что дает применение хореографии:

  • Реализация слабой связанности между компонентами системы.
  • Делает систему гибкой и масштабируемой.

Что дает применение State Machine:

  • Определяет допустимые переходы между состояниями.
  • Изолирует бизнес-логику от реализации.
  • Повышает читаемость кода.
  • Повышает тестируемость.

State Machine для управления состоянием на основе событий

Рассмотрим использование State Machine для управления состоянием, когда сервис участвует в обработке событий. Примерная схема обработки событий:

State Machine и Хореография

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

State Machine и Хореография

В соответствии с этой диаграммой состояния заказов и события выраженные в коде:

// Состояние заказа public enum OrderState { NEW, PAID, SHIPPED, COMPLETED } // События заказа public enum OrderEvent { PAYMENT_COMPLETED, SHIPMENT_STARTED, ORDER_DELIVERED }

Теперь настроим State Machine:

@Configuration @EnableStateMachineFactory @RequiredArgsConstructor public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<OrderState, OrderEvent> { private final OrderPaymentCompleteAction paymentCompleteAction; private final OrderShipmentStartedAction shipmentStartedAction; private final OrderDeliveredAction orderDeliveredAction; @Override public void configure(StateMachineStateConfigurer<OrderState, OrderEvent> configurer) throws Exception { configurer .withStates() .initial(OrderState.NEW) .end(OrderState.COMPLETED) .states(EnumSet.allOf(OrderState.class)); } @Override public void configure(StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions) throws Exception { transitions .withExternal() .source(OrderState.NEW) .target(OrderState.PAID) .event(OrderEvent.PAYMENT_COMPLETED) .action(paymentCompleteAction) .and() .withExternal() .source(OrderState.PAID) .target(OrderState.SHIPPED) .event(OrderEvent.SHIPMENT_STARTED) .action(shipmentStartedAction) .and() .withExternal() .source(OrderState.SHIPPED) .target(OrderState.COMPLETED) .event(OrderEvent.ORDER_DELIVERED) .action(orderDeliveredAction); } }

Здесь OrderPaymentCompleteAction, OrderShipmentStartedAction и OrderDeliveredAction — действия, которые будут выполняться при переходе в соответствующее состояние. Именно в них мы будем реализовывать бизнес-логику. Для иллюстрации действия просто будут выводить в лог сообщение о том, что действие выполняется.

@Slf4j @Component public class OrderDeliveredAction implements Action<OrderState, OrderEvent> { @Override public void execute(StateContext<OrderState, OrderEvent> context) { log.info("Processing Order Delivered Event, message data: {}", context.getExtendedState().getVariables()); log.info("Order delivered"); } }

При создании заказа мы создаем State Machine и сохраняем необходимые данные в контекст. После старта и обработки сохраняем сам контекст State Machine.

@Service @RequiredArgsConstructor public class CreateOrderService { private final StateMachineFactory<OrderState, OrderEvent> stateMachineFactory; private final StateMachinePersister<OrderState, OrderEvent, String> persister; public void createOrder(UUID orderId, UUID productId, BigDecimal price) { log.info("Creating order {}", orderId); StateMachine<OrderState, OrderEvent> stateMachine = stateMachineFactory.getStateMachine(); // Сохраняем данные в extended state (контекст) stateMachine.getExtendedState().getVariables() .putAll(Map.of(Order.Fields.productId, productId, Order.Fields.price, price, Order.Fields.id, orderId)); stateMachine.start(); try { // Сохраняем состояние State Machine persister.persist(stateMachine, orderId.toString()); } catch (Exception e) { log.error("Error persisting order state machine", e); } } }

Для сохранения состояния State Machine используем StateMachinePersister. Чтобы реализовать поведение сохранения и загрузки состояния State Machine, нам нужно реализовать интерфейс StateMachinePersist:

@Slf4j @Component public class OrderStateMachinePersist implements StateMachinePersist<OrderState, OrderEvent, String> { private final HashMap<String, StateMachineContext<OrderState, OrderEvent>> contexts = new HashMap<>(); @Override public void write(final StateMachineContext<OrderState, OrderEvent> context, String orderId) { contexts.put(orderId, context); } @Override public StateMachineContext<OrderState, OrderEvent> read(final String orderId) { StateMachineContext<OrderState, OrderEvent> stateMachineContext = contexts.get(orderId); if (Objects.isNull(stateMachineContext)) { throw new RuntimeException("No context found for order " + orderId); } return stateMachineContext; } }

Но чтобы он заработал, нам необходимо также определить bean (без этого не будет сохраняться) StateMachinePersister:

@Bean public StateMachinePersister<OrderState, OrderEvent, String> persister() { return new DefaultStateMachinePersister<>(new OrderStateMachinePersist()); }

Сам процесс обработки событий можно завернуть в отдельный сервис, который будет восстанавливать состояние State Machine на основе идентификатора заказа, пересылать ей сообщение и сохранять состояние:

@Slf4j @Service @RequiredArgsConstructor public class ProcessOrderEventsService { private final StateMachineFactory<OrderState, OrderEvent> stateMachineFactory; private final StateMachinePersister<OrderState, OrderEvent, String> persister; public void handleEvent(UUID orderId, OrderEvent event, Map<String, String> eventData) { StateMachine<OrderState, OrderEvent> orderStateMachine = stateMachineFactory.getStateMachine(); try { // Восстанавливаем состояние State Machine persister.restore(orderStateMachine, orderId.toString()); } catch (Exception e) { log.error("Error restoring order state machine", e); } orderStateMachine.getExtendedState().getVariables().putAll(eventData); // пересылаем событие orderStateMachine.sendEvent(MessageBuilder.withPayload(event).build()); try { // сохраняем состояние State Machine persister.persist(orderStateMachine, orderId.toString()); } catch (Exception e) { log.error("Error persisting order state machine", e); } } }

События могут как отправляться через очередь сообщений, так и через REST API. Также в зависимости от выбранной архитектуры, для каждого события может быть как отдельный обработчик, так и один обработчик для всех событий. Для примера я реализовал обработку событий через REST API с единственным методом, принимающим тип события и данные события.

Преимущества подхода

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

Используя State Machine, мы берем лучшее из двух миров: независимость и разделение ответственности между участниками процесса (хореография) и централизованную логику переходов между состояниями самих участников.

Исходные коды доступны по ссылке.

Подписывайся на мой telegram-канал

4 комментария