State Machine for Java
implementation 'com.tsm4j:tsm4j:0.1.0'
<dependency>
<groupId>com.tsm4j</groupId>
<artifactId>tsm4j</artifactId>
<version>0.1.0</version>
</dependency>
Tsm4j tries to provide a simple and intuitive state machine model with minimum configuration. The definition of state machine is just states and transitions that will be executed when some states is reached.
An example of a class holding some states:
private static class MyState {
public static final State<Void> HUNGRY = State.create();
public static final State<Void> NO_FOOD = State.create();
public static final State<Void> FOOD_IS_NOT_READY = State.create();
public static final State<Void> MAKE_FOOD = State.create();
public static final State<Void> FOOD_IS_READY = State.create();
public static final State<Void> NOT_HUNGRY = State.create();
public static final State<Integer> ATTEMPTS = State.create();
}
An example of defining and running a state machine:
StateMachine stateMachine = StateMachineBuilder.newInstance()
.addTransition(setOf(MyState.HUNGRY, MyState.NO_FOOD), context -> context.send(MyState.MAKE_FOOD))
.addTransition(MyState.MAKE_FOOD, context -> {
Supplier<Boolean> tryMakeFood = () -> true;
if (tryMakeFood.get()) {
context.send(MyState.FOOD_IS_READY);
} else {
context.send(MyState.FOOD_IS_NOT_READY);
}
})
.addTransition(setOf(MyState.HUNGRY, MyState.FOOD_IS_READY), context -> context.send(MyState.NOT_HUNGRY))
.build();
assertTrue(stateMachine.send(setOf(MyState.HUNGRY, MyState.NO_FOOD)).hasReached(MyState.NOT_HUNGRY));
Note that all states have a type associated with them, this is to allow you to save and load data from an arbitrary state, To ensure the data is available in some transition, specify that state as a dependency.
// ...
.addTransition(MyState.MAKE_FOOD, context -> {
int attempts = context.getOrDefault(MyState.ATTEMPTS, () -> 0);
if (attempts > 3) {
context.send(MyState.FOOD_IS_READY);
} else {
context.send(MyState.ATTEMPTS, attempts + 1);
context.send(MyState.MAKE_FOOD);
}
})
.addTransition(setOf(MyState.HUNGRY, MyState.FOOD_IS_READY, MyState.ATTEMPTS), context -> {
System.out.println("attempts: " + context.getOrError(MyState.ATTEMPTS));
context.send(MyState.NOT_HUNGRY);
})
.build();
// ...
You can handle exception raised from the state machine by defining an exception handler
StateMachine stateMachine = StateMachineBuilder.newInstance()
.addTransition(MyState.HUNGRY, context -> {
throw new RuntimeException("exception!");
})
.addExceptionHandler(RuntimeException.class, (context, e) -> {
// custom eat food logic
context.send(MyState.NOT_HUNGRY);
})
.build();
assertTrue(stateMachine.send(MyState.HUNGRY).hasReached(MyState.NOT_HUNGRY));
For more examples see tsm4j/test.
- I prefer an intuitive graph model that is easier to debug and learns what's going on.
- I want to merge action and transition, event and state, and see if this simpler model works.
- I want to use data from another state with type safety.
Feel free to open up an issue for a feature request, bug report, or pull request.