Skip to content

State Machine

Danila Rassokhin edited this page Jan 27, 2023 · 1 revision

State Machine

State machine provides mechanism to control state flow of some object.

Basic state machine

To create basic state machine use BasicStateMachine.create() method:

UUID id = UUID.randomUUID();

StateMachine<ExampleState, ExampleEvent, UUID> stateMachine = BasicStateMachine.<ExampleState, ExampleEvent, UUID> create()
// Create new transition from STATE1 to STATE2
        .transition(ExampleState.STATE1, ExampleState.STATE2)
// Event, on which transition will be made
// e.g. If current state is STATE1 and EVENT1 will posted to machine, then STATE1 will be changed to STATE2
        .event(ExampleEvent.EVENT1)
// Save current transition and create new one
        .and()
        .transition(ExampleState.STATE2, ExampleState.STATE3)
        .event(ExampleEvent.EVENT2)
        .and()
        .transition(ExampleState.STATE3, ExampleState.STATE1)
        .event(ExampleEvent.EVENT3)
// Specify object which state will be controlled by this machine
        .forPayload(id)
// Specify initial state of machine
        .build(ExampleState.STATE1);

Change state

There are 2 methods to change state:

  • post(E event)
    • This method will try to change state and if there is no transition found for given event, then nothing will happen (Unsafe)
    • e.g. in previous example, if current state is STATE2 and EVENT1 will be posted, so nothing will happen, cause machine expects EVENT2 to change state
  • fire(E event)
    • This method will try to change state and if there is no transition found for given event, then will throw StateMachineException (Safe)
    • e.g. in previous example, if current state is STATE2 and EVENT1 will be posted, so StateMachineException will be thrown, cause machine expects EVENT2 to change state

Example

For previous example:

stateMachine.post(ExampleEvent.EVENT1); // Will change state to ExampleState.STATE2
stateMachine.fire(ExampleEvent.EVENT3); / Will throw StateMachineException

Guards

While creation of transitions you can specify StateMachineGuard for each transition. Guard is a predicate which will be evaluated before transition change and if its value is true, then StateMachineException will be thrown. Guards can protect your state machine transitions from unexpected external events.

Example

// Implement StateMachineGuard interface
  private StateMachineGuard<ExampleState, ExampleEvent> createGuard() {
    return new StateMachineGuard<ExampleState, ExampleEvent>() {
// Define your condition
      @Override
      public boolean test(StateMachineTransition<ExampleState, ExampleEvent> transition) {
        return false;
      }
// You can specify guard name. It will be used in exceptions (optional)
      @Override
      public String getName() {
        return "Example guard";
      }
    };
  }

StateMachine<ExampleState, ExampleEvent, UUID> stateMachine = BasicStateMachine.<ExampleState, ExampleEvent, UUID> create()
        .transition(ExampleState.STATE1, ExampleState.STATE2)
        .event(ExampleEvent.EVENT1)
        .and()
        .transition(ExampleState.STATE2, ExampleState.STATE3)
        .event(ExampleEvent.EVENT2)
// Will protect only transition STATE2 -> STATE3, but not other transitions
        .guard(createGuard())
        .and()
        .transition(ExampleState.STATE3, ExampleState.STATE1)
        .event(ExampleEvent.EVENT3)
        .forPayload(id)
        .build(ExampleState.STATE1);

Actions

Each transition can trigger some action BEFORE changing state. Actions can be specified during transition creation

Example

  // Will print out transition
  private void printTransition(StateMachineTransition<ExampleState, ExampleEvent> transition) {
    BasicComponentManager.getGameLogger()
        .info("Moved from " + transition.getFrom() + " to " + transition.getTo());
  }
StateMachine<ExampleState, ExampleEvent, UUID> stateMachine = BasicStateMachine.<ExampleState, ExampleEvent, UUID>create()
        .transition(ExampleState.STATE1, ExampleState.STATE2)
        .event(ExampleEvent.EVENT1)
        .action(this::printTransition)
        .forPayload(id)
        .build(ExampleState.STATE1)
stateMachine.post(EVENT1); //Will print: Moved from STATE1 to STATE2

Persister (Save\Load states)

StateMachine can save and load current state for object by using implementation of StateMachinePersister. It has 2 methods:

  • persist(P payload, StateMachineTransition<S, E> transition)
    • Will be called BEFORE transition change to save current state for payload
  • getCurrentState(P payload)
    • Should return current state for given payload. Will be called on machine creation to rewrite initial state You can use persister to save states of some objects, e.g. in your database

Example

// Persister for Game object
// Just prints states of Game
private StateMachinePersister<GameState, GameEvent, BasicGame> createPersister() {
    return new StateMachinePersister<GameState, GameEvent, BasicGame>() {
      @Override
      public void persist(BasicGame payload,
                          StateMachineTransition<GameState, GameEvent> transition) {
        // Check if next game state is STOPPED, so game is over
        if (transition.getTo().equals(GameState.STOPPED)) {
          BasicComponentManager.getGameLogger().info("Saving game...");
          // Get all game objects, so we can save them somehow
          List<GameObject> allObjects = payload.getAllGameObjects();
        }
      }

      @Override
      public GameState getCurrentState(BasicGame payload) {
        BasicComponentManager.getGameLogger().info("Loading game state...");
        // We can load game state here
        return GameState.UNDEFINED;
      }
    };
  }
// Use persister for Game state machine
Game game = new BasicGame(createPersister());