This is a heavy WIP, nonetheless it works well in our production code. To make the state machine as 'DSL' like as possible i've tried to take advantage of mixins to allow one class to 'act_as' a state machine. This part hasn't worked out so well, but it is my intention to make it happen if at all possible.
At the outset I must tell you, none of the code on this page is tested. I'll do that some other time :P
All code is under the MIT license.
- MXML Markup support? maybe? We don't have a specific use for this.
- Mixin behaviour so that it can 'act_as'
- Multiple to/from states in one even definition (still creates multiple transitions)
This is lifted from production code, I think it's elegant but I do invite criticism.
static public const IDLE:String = "idle";
static public const COMMITTING:String = "committing";
static public const EDITING:String = "editing";
private function configureStateMachine():void {
machine = new StateMachine();
machine.addState(
COMMITTING,
{entry:setBusy,exit:clearBusy}
);
machine.addState(EDITING);
machine.addState(IDLE);
machine.select =
{fromState:IDLE, toState:IDLE, actions:select};
machine.idle =
{fromState:COMMITTING, toState:IDLE, actions:clearEdit};
machine.idle =
{fromState:EDITING, toState:IDLE, actions:clearEdit};
machine.construct =
{fromState:IDLE, toState:EDITING, actions:construct};
machine.edit =
{fromState:IDLE, toState:EDITING, actions:edit, guards:selectedResourceExists};
machine.edit =
{fromState:EDITING, toState:IDLE, guards:selectedResourceExists};
machine.edit =
{fromState:COMMITTING, toState:IDLE, guards:selectedResourceExists};
machine.dispose =
{fromState:IDLE, toState:COMMITTING, actions:dispose, guards:selectedResourceExists};
machine.dispose =
{fromState:EDITING, toState:IDLE, guards:selectedResourceExists};
machine.dispose =
{fromState:COMMITTING, toState:IDLE, guards:selectedResourceExists};
machine.disposeSelected =
{fromState:IDLE, toState:COMMITTING, actions:disposeSelected, guards:selectedResourceExists};
machine.editSelected =
{fromState:IDLE, toState:EDITING, actions:editSelected, guards:selectedResourceExists};
machine.save =
{fromState:EDITING, toState:COMMITTING, actions:saveEdit};
machine.follow(this,'synchronousStatus');
machine.setInitialState(IDLE);
}
I assume you know how state machines work, and know what a 'State', 'Event', 'Transition', 'Action' and 'Guard' are.
Normally all your prep of the state machine will be done in one method. Dynamic state machines are too hard to keep track of :)
machine = new StateMachine();
machine.addState("start");
machine.addState("running");
You know, with constants.
static const START:String = "start";
static const RUNNING:String = "running";
... later ...
machine.addState(START);
machine.addState(RUNNING);
machine.setInitialState(START);
Entry and exit actions support parameters, see Actions support parameters.
machine.addState(
COMMITTING,
{entry:setBusy,exit:clearBusy}
);
machine.addState(
COMMITTING,
{
entry:[setBusy,clearScreen],
exit:[clearBusy,populateScreen]
}
);
machine.begin = {fromState:START, toState:RUNNING};
Creates an event "begin", which triggers a transition from START to RUNNING
machine.toggle = {fromState:A, toState:B};
machine.toggle = {fromState:B, toState:A};
You may find you want to do this:
machine.quit = {
fromState:[WAITING,MENU,FIRING],
toState:FINISHED
};
But alas that's not supported yet. You need:
machine.quit = {fromState:WAITING, toState:FINISHED};
machine.quit = {fromState:MENU, toState:FINISHED};
machine.quit = {fromState:FIRING, toState:FINISHED};
Trigger a transition by calling the event. First the event is defined:
machine.quit = {fromState:WAITING, toState:FINISHED};
If the current state is WAITING, then calling:
var success:Boolean = machine.quit();
causes state to change from WAITING to FINISHED.
Transitions do not occur if the current state does not match a fromState for the event being called. In this case, for example, if the state was RUNNING, calling quit() does nothing.
Success is shown in the result of the event trigger.
machine.quit = {fromState:WAITING, toState:FINISHED, actions:_quit};
...later...
private function _quit():void {
...
called when the transition occurs (state changes)
...
}
_quit is only called if the event can cause the transition.
machine.quit = {fromState:WAITING, toState:FINISHED, actions:[_quit,_other]};
...later...
private function _quit():void {
... do stuff ...
}
private function _other():void {
... then this ...
}
You can call an event with parameters which are passed to actions.
machine.quit = {fromState:WAITING, toState:FINISHED, actions:_quit};
...later...
private function _quit(message:String = null):void {
trace("Quitting with: " + message);
}
Called with
machine.quit("You Lose");
will output
Quitting with: You Lose
machine.disposeSelected = {}
fromState:IDLE,
toState:COMMITTING,
actions:disposeSelected,
guards:selectedResourceExists
};
selectedReesourceExists must return true or false. If it returns true, and the fromState is true, the transition will occur.
Guards support parameters, see Actions support parameters.
machine.disposeSelected = {}
fromState:IDLE,
toState:COMMITTING,
actions:disposeSelected,
guards:[selectedResourceExists,selectedResourceValid]
};
You can test if an event would cause a transition with 'cans':
Assume the quit event is declared:
machine.quit = {fromState:WAITING, toState:FINISHED};
You can check if calling quit() will cause a transition with
trace(machine.quit);
or preferably:
trace(machine.canQuit)
See how these are not method calls, just property inspection. I prefer canQuit, it reads a lot better than quit.
'cans' return true if the transition will complete.
Be aware, guards are included in the 'can'.
Common use case for 'cans' are as flags to enbable UI elements. for example:
<mx:Button label="Quit" enabled="{machine.canQuit}" >
Rasheed Abdul-Aziz, Visfleet.