HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavaMinor

Finite State Machine supporting shortest path transitions

Submitted by: @import:stackexchange-codereview··
0
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:

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 from calls like builder.from(KID).from(ADULT)? I know I could introduce another class as return value for the first call to form, and using TransitionAdder only for to, 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 use LinkedLists if I want to do this, or is

Solution

Runnable Review

From Reference Docs:


When an object implementing interface Runnable is used to create a
thread, starting the thread causes the object's run method to be
called 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 state


I 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 class
as return value for the first call to form, and using TransitionAdder
only 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 state
StateMachineBuilder
    .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.