patternjavaMinor
Finite State Machine supporting shortest path transitions
Viewed 0 times
transitionspathfiniteshorteststatesupportingmachine
Problem
In our automated test framework, written in Java 8, there are different entities representing test data, having different states and transitions between them.
To model this behavior, I started to implement a simple finite state machine (or at least what I understand as a FSM).
The idea to use it would be like this:
The reason I chose to write an implementation by myself was that ie. stateless4j doesn't support going directly from UNBORN to ADULT, because it doesn't look for a shortest path.
Although I'm thankful for every feedback I can get, I'm mostly thinking about the following points:
To model this behavior, I started to implement a simple finite state machine (or at least what I understand as a FSM).
The idea to use it would be like this:
public class Example {
private enum Human {
UNBORN, BORN, KID, ADULT, DEAD
}
@Test
public void test() {
StateMachine fsm = StateMachineBuilder.create(Human.class)
.from(Human.UNBORN).to(Human.BORN).to(Human.DEAD, this::died)
.from(Human.BORN).to(Human.KID).to(Human.DEAD, this::died)
.from(Human.KID).to(Human.ADULT).to(Human.DEAD, this::died)
.from(Human.ADULT).to(Human.DEAD).startAt(Human.UNBORN);
// dying as an unborn :(
fsm.go(Human.DEAD);
// going from UNBORN to BORN, KID, ADULT
fsm.reset();
fsm.go(Human.ADULT);
}
private void died() {
System.out.println("Oh no :(");
}
}The reason I chose to write an implementation by myself was that ie. stateless4j doesn't support going directly from UNBORN to ADULT, because it doesn't look for a shortest path.
Although I'm thankful for every feedback I can get, I'm mostly thinking about the following points:
- Is my state machine a state machine? The input it receives aren't any triggers or so, but a target state it should transition to.
- Is my builder really a builder?
- Is there a simple way to not allow multiple
fromcalls likebuilder.from(KID).from(ADULT)? I know I could introduce another class as return value for the first call toform, and usingTransitionAdderonly forto, but it seems like an overkill.
- Are the names okay? In particular, I'm unhappy with
TransitionAdder.
- Have I missed important information in the Javadoc?
shortestRoute.get().add(0, from);- should I useLinkedLists if I want to do this, or is
Solution
Runnable Review
From Reference Docs:
When an object implementing interface
thread, starting the thread causes the object's
called in that separately executing thread.
This goes completely against the run-to-completion model of a state machine.
State Machine Review
Is my state machine a state machine? The input it receives aren't any triggers or so, but a target state it should transition to.
There is no single formal definition of a state machine. Different specifications such as Mealy, Moore and UML exist. And even within these specifications, you are not limited to using formal constraints. So yes, your state machine is a state machine, as you have states, transitions and a run-to-completion flow
If you would have picked a Moore machine instead,
I do have a couple of considerations to make this state machine more robust:
Error Handling
When no valid transition is found, you
Transition Integrity
When transitioning, you are calling the transition behavior
Builder Review
Is my builder really a builder?
You have designed a fluent API for building a state machine with its transitions. This definately is a builder pattern.
I would indent and nest each builder method, though, to make the hierarchy more clear.
Is there a simple way to not allow multiple from calls like
as return value for the first call to form, and using
only for to, but it seems like an overkill.
It's unfortunate you find your own suggestion overkill. You should have indeed split the
From Reference Docs:
When an object implementing interface
Runnable is used to create athread, starting the thread causes the object's
run method to becalled in that separately executing thread.
This goes completely against the run-to-completion model of a state machine.
Runnable is implied to run in a dedicated thread, while your state machine operations (like transitions) are meant to run in the main state machine thread. So I suggest to create a custom ITransition interface.State Machine Review
Is my state machine a state machine? The input it receives aren't any triggers or so, but a target state it should transition to.
There is no single formal definition of a state machine. Different specifications such as Mealy, Moore and UML exist. And even within these specifications, you are not limited to using formal constraints. So yes, your state machine is a state machine, as you have states, transitions and a run-to-completion flow
go. In fact, your state machine is a subset of a Mealy machine, where transitions are the driver for state changes. This can be seen in go:log.trace("Going to state " + state);
runnables.forEach(Runnable::run); // Mealy flow with transitions
currentState = state;If you would have picked a Moore machine instead,
go would have looked like this:log.trace("Going to state " + state);
currentState.exit(); // Moore machine Exit old state
currentState = state;
currentState.enter(); // Moore machine Enter new stateI do have a couple of considerations to make this state machine more robust:
Error Handling
When no valid transition is found, you
throw IllegalArgumentException("There is no valid transition!") leaving the state machine in the current state. Who should handle the error? In a run-to-completion model, it is expected that each run step completes, even if an error occurs. I would opt adding an error state with a default transition to this state on any error in any other state.Transition Integrity
When transitioning, you are calling the transition behavior
runnables.forEach(Runnable::run) be careful that anything may happen in these operations, including throwing errors (already discussed in the previous point) and other state changes (calling go inside a transition). This latter flow is violating the flow integrity, as the current transitions get put on hold, new transitions take place, and then the remaining previous (obsolete) transitions continue, resulting in unwanted transitions. This could be solved by adding a boolean isTransitioninig in go and throwing an error when go is called from within another ongoing transition.Builder Review
Is my builder really a builder?
You have designed a fluent API for building a state machine with its transitions. This definately is a builder pattern.
StateMachineBuilder
.create(Human.class)
.from(Human.UNBORN)
.to(Human.DEAD, this::died);I would indent and nest each builder method, though, to make the hierarchy more clear.
Is there a simple way to not allow multiple from calls like
builder.from(KID).from(ADULT)? I know I could introduce another classas return value for the first call to form, and using
TransitionAdderonly for to, but it seems like an overkill.
It's unfortunate you find your own suggestion overkill. You should have indeed split the
TransitionAdder up in a TransitionFromBuilder and TransitionToBuilder. This way, you could preserve integrity by removing from() from TransitionFromBuilder and having to() return a TransitionFromBuilder. You could always create a public reference back to the previous level. From TransitionToBuilder you should be able to go back to TransitionFromBuilder and from TransitionFromBuilder back to StateMachineBuilder.Code Snippets
log.trace("Going to state " + state);
runnables.forEach(Runnable::run); // Mealy flow with transitions
currentState = state;log.trace("Going to state " + state);
currentState.exit(); // Moore machine Exit old state
currentState = state;
currentState.enter(); // Moore machine Enter new stateStateMachineBuilder
.create(Human.class)
.from(Human.UNBORN)
.to(Human.DEAD, this::died);Context
StackExchange Code Review Q#92162, answer score: 3
Revisions (0)
No revisions yet.