A really easy to use but useful State Machine implementation with zero dependencies.
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
.
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
String id = "turnstile0-1";
stateMachine.start(id);
...
stateMachine.post(id, STATE_UNLOCKED);
...
stateMachine.post(id, STATE_LOCKED);
The parameter
id
ofstart()
orpost()
identifies the object that using this state machine, which means different ids have their own state. If states change with payload, callpostWithPayload
methods with payload, likepostWithPayload(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);
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 ofStateMachine
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.
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);
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());
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
}
- 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>