1.2 The State Machine Model
Once the team has decided that a state machine is a good abstraction for specifying how the controllers work, the next step is to ensure that abstraction is put into the software itself. If people want to think about controller behavior with events, states, and transitions, then we want that vocabulary to be present in the software code too. This is essentially the Domain-Driven Design principle of Ubiquitous Language [Evans DDD]—that is, we construct a shared language between the domain people (who describe how the building security should work) and programmers.
When working in Java, the natural way to do this is through a Domain Model [Fowler PoEAA] of a state machine.
Figure 1.2 Class diagram of the state machine framework
The controller communicates with the devices by receiving event messages and sending command messages. These are both four-letter codes sent through the communication channels. I want to refer to these in the controller code with symbolic names, so I create event and command classes with a code and a name. I keep them as separate classes (with a superclass) as they play different roles in the controller code.
class AbstractEvent... private String name, code; public AbstractEvent(String name, String code) { this.name = name; this.code = code; } public String getCode() { return code;} public String getName() { return name;} public class Command extends AbstractEvent public class Event extends AbstractEvent
The state class keeps track of the commands that it will send and its outbound transitions.
class State... private String name; private List<Command> actions = new ArrayList<Command>(); private Map<String, Transition> transitions = new HashMap<String, Transition>(); class State... public void addTransition(Event event, State targetState) { assert null != targetState; transitions.put(event.getCode(), new Transition(this, event, targetState)); } class Transition... private final State source, target; private final Event trigger; public Transition(State source, Event trigger, State target) { this.source = source; this.target = target; this.trigger = trigger; } public State getSource() {return source;} public State getTarget() {return target;} public Event getTrigger() {return trigger;} public String getEventCode() {return trigger.getCode();}
The state machine holds on to its start state.
class StateMachine... private State start; public StateMachine(State start) { this.start = start; }
Then, any other states in the machine are those reachable from this state.
class StateMachine... public Collection<State> getStates() { List<State> result = new ArrayList<State>(); collectStates(result, start); return result; } private void collectStates(Collection<State> result, State s) { if (result.contains(s)) return; result.add(s); for (State next : s.getAllTargets()) collectStates(result, next); } class State... Collection<State> getAllTargets() { List<State> result = new ArrayList<State>(); for (Transition t : transitions.values()) result.add(t.getTarget()); return result; }
To handle reset events, I keep a list of them on the state machine.
class StateMachine... private List<Event> resetEvents = new ArrayList<Event>(); public void addResetEvents(Event... events) { for (Event e : events) resetEvents.add(e); }
I don’t need to have a separate structure for reset events like this. I could handle this by simply declaring extra transitions on the state machine like this:
class StateMachine... private void addResetEvent_byAddingTransitions(Event e) { for (State s : getStates()) if (!s.hasTransition(e.getCode())) s.addTransition(e, start); }
I prefer explicit reset events on the machine because that better expresses my intent. While it does complicate the machine a bit, it makes it clear how a general machine is supposed to work, as well as the intention of defining a particular machine.
With the structure out of the way, let’s move on to the behavior. As it turns out, it’s really quite simple. The controller has a handle method that takes the event code it receives from the device.
class Controller... private State currentState; private StateMachine machine; public CommandChannel getCommandChannel() { return commandsChannel; } private CommandChannel commandsChannel; public void handle(String eventCode) { if (currentState.hasTransition(eventCode)) transitionTo(currentState.targetState(eventCode)); else if (machine.isResetEvent(eventCode)) transitionTo(machine.getStart()); // ignore unknown events } private void transitionTo(State target) { currentState = target; currentState.executeActions(commandsChannel); } class State... public boolean hasTransition(String eventCode) { return transitions.containsKey(eventCode); } public State targetState(String eventCode) { return transitions.get(eventCode).getTarget(); } public void executeActions(CommandChannel commandsChannel) { for (Command c : actions) commandsChannel.send(c.getCode()); } class StateMachine... public boolean isResetEvent(String eventCode) { return resetEventCodes().contains(eventCode); } private List<String> resetEventCodes() { List<String> result = new ArrayList<String>(); for (Event e : resetEvents) result.add(e.getCode()); return result; }
It ignores any events that are not registered on the state. For any events that are recognized, it transitions to the target state and executes any commands defined on that target state.