5
\$\begingroup\$

I wrote a transition table which allows the client to:

  1. Create States
  2. Create a TransitionTable or some kind of StateManager
  3. Create StateNodes using that table

A StateNode is what I use to link one state to other states. It has the mainState and an ArrayList<State> full of states it can jump to.

After creating nodes, the client is able to run a loop, grabbing the current state from the table and processing it by calling process, passing in:

  • The application
  • The table, to make transitions

The application type may be different, so each state needs to be created for that specific application, which can be done using the generic type parameter I gave to State. The TransitionTable is then given the type of states it will be managing, using the generic type parameter I gave it.

From my perspective, this is highly scalable, compared to enum state systems. The client can plug-in states, and different state types can be extended upon for organization. I have yet to get into annotation processing, so that's why clients must use marker interfaces for cleaner code.

This is the the framework they will be building upon

Application

public abstract class Application {
    private AtomicBoolean running = new AtomicBoolean(false);
    private Thread thread;

    public final void start() {
        if(running != null && running.compareAndSet(false, true)) {
            init();

            thread = new Thread(logic);
            thread.start();
        }
    }

    private Runnable logic = () -> {
        while(running.get()) {
            process();
        }
    };

    protected abstract void init();
    protected abstract void process();
}

TransitionTable with StateNode nested inside:

public final class TransitionTable<T extends State<?>> {
    private Map<T, StateNode> nodes = new HashMap<>();
    private StateNode currentNode, startNode;

    public StateNode createNode(T state) {
        StateNode node = new StateNode(state);
        nodes.put(state, node);
        if(startNode == null) {
            startNode = currentNode = node;
        }
        return node;
    }

    public void transitionTo(int index) {
        T nextState = currentNode.containsTransition(index) ? currentNode.states.get(index) : startNode.mainState;
        currentNode = nodes.get(nextState);
    }

    public T getCurrentState() {
        return currentNode.mainState;
    }

    public class StateNode {
        private final ArrayList<T> states = new ArrayList<>();
        private final T mainState;

        private StateNode(T state) {
            this.mainState = state;
        }

        private boolean containsTransition(int index) {
            return index >= 0 && index <= states.size();
        }

        public void addTransitions(T...states) {
            for(T state : states) {
                this.states.add(state);
            }
        }
    }
}

State

interface State<T extends Application> {
    void process(T app, TransitionTable<?> table);
}

This is how the client will use the API:

Launcher

public class Launcher {
    public static void main(String[] args) {
        Application app = new DemoApplication();
        app.start();
    }
}

DemoState (the marker interface)

public interface DemoState extends State<DemoApplication> { }

DemoApplication

public final class DemoApplication extends Application {
    private TransitionTable<DemoState> table;

    protected void init() {
        DemoState start = new StartState();
        DemoState walkToHouse = new WalkToHouseState();
        DemoState walkToWork = new WalkToWorkState();
        DemoState sleep = new SleepState();
        DemoState work = new WorkState();

        table = new TransitionTable<>();
        table.createNode(start).addTransitions(walkToHouse, walkToWork);
        table.createNode(walkToHouse).addTransitions(sleep);
        table.createNode(walkToWork).addTransitions(work);
        table.createNode(sleep).addTransitions(walkToWork);
        table.createNode(work).addTransitions(walkToHouse);
    }

    protected void process() {
        table.getCurrentState().process(this, table);
    }

    /* This is for demonstration purposes only */
    public boolean walkingHome = true, walkingToWork;
    public int workLocation = 0, homeLocation = 100, currentLocation;
    //Other states..

    public boolean atWork() {
        return currentLocation == workLocation;
    }

    public boolean atHome() {
        return currentLocation == homeLocation;
    }
}

StartState

public class StartState implements DemoState {
    @Override
    public void process(DemoApplication app, TransitionTable<?> table) {
        System.out.println("Starting");
        if(app.walkingHome) {
            table.transitionTo(0);
        } else if(app.walkingToWork) {
            table.transitionTo(1);
        }
    }
}

WalkToHouseState

public class WalkToHouseState implements DemoState {
    @Override
    public void process(DemoApplication app, TransitionTable<?> table) {
        if(app.atHome()) {
            table.transitionTo(0);
        } else {
            System.out.println("Took a step towards home");
            app.currentLocation++;
        }
    }
}

WalkToWorkState

public class WalkToWorkState implements DemoState {
    @Override
    public void process(DemoApplication app, TransitionTable<?> table) {
        if(app.atWork()) {
            table.transitionTo(0);
        } else {
            System.out.println("Took a step towards the workplace");
            app.currentLocation--;
        }
    }
}

WorkState

public class WorkState implements DemoState {
    @Override
    public void process(DemoApplication app, TransitionTable<?> table) {
        System.out.println("Working...");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        table.transitionTo(0);
    }
}

SleepState

public class SleepState implements DemoState {
    @Override
    public void process(DemoApplication app, TransitionTable<?> table) {
        System.out.println("Sleeping");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        table.transitionTo(0);
    }
}

What I don't like about my current code

  1. When the client creates a node and add transitions to it, you must remember the order you added the transitions in, and how many transitions you added. I don't like this aspect, as it requires you to take a mental note of something, which limits scalability to how much the person can easily remember. How could I get around this without affecting performance?

  2. This seems a bit verbose. I like how simple-minded it is, but personally I find it to be a lot of code. Is there a clean way to cut it down, or am I just being paranoid?

I'm accepting all opinions on anything (except using Optional). If you feel I should do something differently to increase performance, increase scalability or decrease verbosity PLEASE let me know.

\$\endgroup\$
4
\$\begingroup\$

Typically, in a GoF State pattern implementation, the StateContext (TransitionTable in this case) doesn't track the transitions. The states are responsible for handling the transition logic and notifying the context. Why are you taking that burden onto your library? I'm missing the design goal here, and that makes it hard to critique your technical decisions.

Is this really all SPI, and your library is making the calls into TransitionTable? Even in that case, I'm not sure why you need to manage the transitions yourself, except to try to force the client to correctly implement a state machine. Either your clients understand a state machine and that's just an annoying hoop, or they don't and it won't help them very much at all.

It's also not really clear what the scope of these state machines is going to be. If clients will have 5 or 6 states, that's a very different problem to solve than 500 or 600 states. You said you wanted scalable - to what size?

Anyway, the reason you're having issue #1 is because your library insists on knowing the state transitions, even though your only value-add is checking that a requested transition was defined for the state. If you insist on managing the transitions, I don't think there's a nice way to get around #1.

Based on what I've seen, and with no knowledge whatsoever of your business requirements, my suggestion would be to leave it up to the client to handle the state transitions. You're right that you can't use an enum, but they sure can. Your absolute best bet is to take this, stick it in front of a dev from each of your three favorite clients, and ask them to write a representative implementation for their needs. Believe me, you'll find out where your weaknesses are.

Some thoughts:

Application#start() - You don't need the null check running != null unless you've forgotten to include running = null when the thread ends.

State - Strongly consider adding a getTransitions() method that returns the available transitions. This will fix the cumbersome table.addNode().addTransitions(), and let you hide the existence of StateNode, which clients have no business knowing about. This will also fix the need for clients to remember which transitions were configured, since now they're defined in the same State implementation that uses them. In this case, clients would need to use an enum, or some container of states that all states have access to, or write stateless states so they can instantiate instances as needed.

TransitionTable#createNode() - Do clients really need to know about nodes? Why not addState()?

DemoApplication#init() - WalkToWork should be WalkingToWork, Sleep should be Sleeping, etc. Only relevant if you intend to release this demo code.

DemoApplication#process() - I'd wager that most clients will want exactly this, and I think those that don't are wrong. That should be part of your library.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ The states CAN'T manage which state they transition to because they don't have a reference to the state; that's why I'm asking for a way around it, without resorting to enums or creating a static getter for all my state classes. I'm an independent dev, no business requirements. My clients do not have the knowledge to create a state machine of their own, as their programming design and logic skills lack. I was going to encapsulate the table within Application, then pass it to the subtype with an abstract method, allowing the client to create transitions without needing.. \$\endgroup\$ – Dioxin Dec 31 '14 at 2:50
  • \$\begingroup\$ to declare static methods in their own state classes (using some kind of singleton pattern). Static acces is the only way I see to get around the need to centeralize my states in such a way, but I do not want to force my clients to have the knowledge to make such declarations. Once again, I'm the only developer on this project, so I do not have other people to go to for views, other than a few people I've introduced to the language. As for hiding StateNode, I was thinking of a way to do that. I guess I could return the ArrayList of states, but then I wouldn't have the simplicity of varags \$\endgroup\$ – Dioxin Dec 31 '14 at 2:51
  • \$\begingroup\$ @VinceEmigh I understand that. My point is, that lack of reference is a client problem, not an SPI problem. You can't solve it in the SPI. The client can solve it with an enum that implements state, or a bunch of public static final instances, or by making new StateImpl instances iff the StateImpls are themselves stateless. So it seems like the real problem you're trying to solve is "What's the best way to let a client implement a state machine without understanding state machines"? \$\endgroup\$ – Eric Stein Dec 31 '14 at 3:24
2
\$\begingroup\$

Essentially, as you say, you've written a lot of code to accomplish something intuitively simple. Moreover, you've made various impositions on the user. I wonder whether the problem here lies more with the OO approach.

I suggest separating your concerns more clearly: you have states, events, and transitions causing state changes based on events. Attached to states you have "entry" actions. Attached to transitions you have "change" actions.

Here's how I'd do it in F# -- I think the translation into Java or the language of your choice is pretty straightforward:

// General purpose code.

type Transitions<'state, 'event>
    when 'event: comparison = 
        {
            onEntry: unit -> unit;
            onEvent: Map<'event, 'state>;
            onLeave: 'state * 'event * 'state -> unit
        }

type StateTransitions<'state, 'event> 
    when 'state: comparison and 'event: comparison =
        Map<'state, Transitions<'state, 'event>>

let transition (sts: StateTransitions<'state, 'event>) thisState event =
    let thisStateTn = sts.[thisState]
    let nextState = thisStateTn.onEvent.[event]
    let nextStateTn = sts.[nextState]
    thisStateTn.onLeave (thisState, event, nextState)
    nextStateTn.onEntry()
    nextState

Here's how you might use the above to implement your demonstration:

// Demo code.

type State = Start | WalkingHome | WalkingToWork | Sleeping | Working

type Event = Sleep | WalkHome | WalkToWork | Arrive | Work

let showTn (a, e, b) = printfn "%A -- %A --> %A" a e b

let stateTransitions =
    Map.empty
       .Add(Start, {
           onEntry = fun () -> printfn "Starting";
           onEvent = Map.empty.Add(WalkHome, WalkingHome)
                              .Add(WalkToWork, WalkingToWork);
           onLeave = showTn
        })
       .Add(WalkingHome, {
           onEntry = fun () -> printfn "Heading home";
           onEvent = Map.empty.Add(Arrive, Sleeping);
           onLeave = showTn
        })
       .Add(WalkingToWork, {
           onEntry = fun () -> printfn "Going to work";
           onEvent = Map.empty.Add(Arrive, Working);
           onLeave = showTn
        })
       .Add(Sleeping, {
           onEntry = fun () -> printfn "Zzzzz...";
           onEvent = Map.empty.Add(WalkToWork, WalkingToWork);
           onLeave = showTn
        })
       .Add(Working, {
           onEntry = fun () -> printfn "Slog, slog, slog...";
           onEvent = Map.empty.Add(WalkHome, WalkingHome);
           onLeave = showTn
        })

[<EntryPoint>]
let main argv = 
    let s = Start
    let s = transition stateTransitions s WalkHome
    let s = transition stateTransitions s Arrive
    let s = transition stateTransitions s WalkToWork
    let s = transition stateTransitions s Arrive
    let s = transition stateTransitions s WalkHome
    let s = transition stateTransitions s Arrive
    0 // return an integer exit code

The output from this demo is

Start -- WalkHome --> WalkingHome
Heading home
WalkingHome -- Arrive --> Sleeping
Zzzzz...
Sleeping -- WalkToWork --> WalkingToWork
Going to work
WalkingToWork -- Arrive --> Working
Slog, slog, slog...
Working -- WalkHome --> WalkingHome
Heading home
WalkingHome -- Arrive --> Sleeping
Zzzzz...
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.