Skip to content

A really easy to use but powerful State Machine implementation based on Java 8 with zero dependencies.

License

Notifications You must be signed in to change notification settings

swiftech/SWState

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SWState

A really easy to use but useful State Machine implementation with zero dependencies.

Usage

Assume that we have a turnstile with 2 states Locked, Unlocked and 2 actions Coin, Push, as the diagram shows:

The main class that you use SWState is StateMachine, before using that, you should build it by class StateBuilder.

Build the StateMachine

defined states:

final String STATE_LOCKED = "Locked";
final String STATE_UNLOCKED = "Unlocked";

Use StateBuilder to define Actions and Processes:

StateBuilder<String, Serializable> stateBuilder = new StateBuilder<>();
stateBuilder
        .state(STATE_LOCKED)
        .in(order -> {
            // Handle before the turnstile is locked.
            System.out.println("turnstile is locked");
        })
        .state(STATE_UNLOCKED)
        .in(order -> {
            // Handle before the turnstile is unlocked.
            System.out.println("turnstile is unlocked");
        })
        .initialize(STATE_LOCKED)
        .action("coin_locked", STATE_LOCKED, STATE_UNLOCKED)
        .action("push_unlocked", STATE_UNLOCKED, STATE_LOCKED)
        .action("coin_unlocked", STATE_UNLOCKED, STATE_UNLOCKED)
        .action("push_unlocked", STATE_LOCKED, STATE_LOCKED);
StateMachine<String, Serializable> stateMachine = new StateMachine<>(stateBuilder);

As you can see, we have set up 2 states and 4 actions to change states. the method in() bind your actual processing code block

Use StateMachine to transit states as per previous definitions.

String id = "turnstile0-1";
stateMachine.start(id);
...
stateMachine.post(id, STATE_UNLOCKED);
...
stateMachine.post(id, STATE_LOCKED);

The parameter id of start() or post() identifies the object that using this state machine, which means different ids have their own state. If states change with payload, call postWithPayload methods with payload, like postWithPayload(id, payload).

If no ID is used to identify object, just call methods without ID:

stateMachine.start();
...
stateMachine.post(STATE_UNLOCKED);
...
stateMachine.post(STATE_LOCKED);

Trigger

As of version 2.0, Trigger is introduced to provides automatically state transition. Assume that the states transitions are determined just by some input, with trigger, you don't need to write the logic code by your own, it can be done automatically.

for example, by defining specific input to trigger the state transition:

  • first of all, define the actions like this:
stateBuilder
        ...
        .action("coin_locked", STATE_LOCKED, STATE_UNLOCKED, stateBuilder.triggerBuilder().c('a', 'A').build())
        .action("push_unlocked", STATE_UNLOCKED, STATE_LOCKED, stateBuilder.triggerBuilder().i(1).build())
        .action("coin_unlocked", STATE_UNLOCKED, STATE_UNLOCKED, stateBuilder.triggerBuilder().f(1.0f).build())
        .action("push_unlocked", STATE_LOCKED, STATE_LOCKED, stateBuilder.triggerBuilder().s("STRING1", "STRING2").build());
  • to activate the states transition automatically, just consume the input using accept method of StateMachine continuously:
    stateMachine.accept('a'); // transit state from STATE_LOCKED to STATE_UNLOCKED
    stateMachine.accept(1); // transit state from STATE_UNLOCKED to STATE_LOCKED
    stateMachine.accept("unkown input") // THIS WON'T TRIGGER ANY STATE TRANSITION. 
  • of course, custom trigger can be defined to handle specific situation, eg:
.action("coin_locked", STATE_LOCKED, STATE_UNLOCKED, stateBuilder.triggerBuilder().c('a', 'A')
        .custom((data, payload) -> {
            return payload.content.equals("ALLOWED");
        )
        .build())

this is equivalent to posting state by post() method.

Advanced

The StateMachine stores states in memory by default, if you want to store states into other storages like database or nosql, there are 2 ways to get this done, implement a StateProvide or use StateTransition directly.

Example: Assume that we have a simplified online shopping order processing with some order states, as the diagram shows:

defined states

final String STATE_CREATED = "Created";
final String STATE_PAYED = "Payed";
final String STATE_CANCELED = "Canceled";
final String STATE_RECEIVED = "Received";

Set up Actions and Process with StateBuilder:

StateBuilder<String, Order> stateBuilder = new StateBuilder<>();
stateBuilder
    .state(STATE_CREATED)
    .in(order -> {
    // Handle the order is created .
    })
    .state(STATE_PAYED)
    .in(order -> {
    // Handle the order is payed.
    })
    .state(STATE_CANCELED)
    .in(order -> {
    // Handle the order is canceled
    })
    .state(STATE_RECEIVED)
    .in(order -> {
    // Handle the delivery
    })
    .initialize("create order", STATE_CREATED)
    .action("pay order", STATE_CREATED, STATE_PAYED)
    .action("cancel order", STATE_CREATED, STATE_CANCELED)
    .action("deliver goods", STATE_PAYED, STATE_RECEIVED);

Method 1: Customized State Provider

To store states, you need to implement a StateProvider, SWState provides a DefaultStateProvider which stores states in memory, but it is probably not suit your situation. Usually, the states you want to manage are in a column of DB tables, so let's implement a database version StateProvider.

MyDatabaseStateProvider.java

import com.github.swiftech.swstate.StateProvider;

public class MyDatabaseStateProvider implements StateProvider<String> {
  public MyDatabaseStateProvider() {
      // do some necessary init
  }
  ... // implement all methods for storing or retrieving state from database.
}

Replace the default state provider of state machine with yours:

stateMachine.setStateProvider(new MyDatabaseStateProvider());

Method 2: Use StateTransition

Instead of StateMachine, StateTransition is at lower level, it doesn't store current state but only process state transition.

First, construct instance of StateTransition just like StateMachine does.

StateTransition<String, Order> stateTransition = new StateTransition<>(stateBuilder);

Second, use stateTransition to transit states which are loaded from other storage:

public void pay(String id){
    String currentState = repository.getState(id); // repository is your own data access API
    stateTransition.post(currentState, STATE_PAYED); // if current state is not 'Created', it fails as per previous setting
}

Maven

  • Stable version

Minimum JDK version is 8

<dependency>
    <groupId>com.github.swiftech</groupId>
    <artifactId>swstate</artifactId>
    <version>1.1</version>
</dependency>
  • Unstable version

Minimum JDK version is 17

<dependency>
    <groupId>com.github.swiftech</groupId>
    <artifactId>swstate</artifactId>
    <version>2.0.1</version>
</dependency>

About

A really easy to use but powerful State Machine implementation based on Java 8 with zero dependencies.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages