The "Gang of Four" design patterns book describes the State pattern and outlines an implementation. The idiom presented here takes a different approach that is based on implementing UML state diagram behavior.
The org.jmonde.state package is used to help implement this idiom. This package provides support for nested states, transitions, history, and enter and exit actions.
This package does not provide for guard conditions, transition actions, or internal actions. These are provided by the idiom as described later in this document.
The classes in this package are not thread-safe. Instances of these classes should be encapsulated and it is the responsibility of the enclosing class to provide thread-safe access.
An instance of the State class is created for each state in the state diagram. The State class is not designed to be subclassed, it is meant to be used as is. However, for debugging, it is useful to override the toString() method to provide a meaningful name.
States can be nested by defining a parent/child relationship.
A child state can be defined to be the autoChild of its parent. This means that if a transition is explicitly made to a parent state that has an autoChild then, after entering the parent state, the autoChild state is automatically entered. You can bypass the autoChild mechanism of the parent by transitioning directly to a child state.
A parent state can also have history. A parent with history will dynamically change it's autoChild to the most-recently entered child.
Application-specific actions can be attached to a state. These actions are called each time a state is entered, exited, or becomes the current state. Actions implement the java.lang.Runnable interface.
The isActive method is called to determine if a state is active (i.e. is currently entered). If a child state is active then the parent is also active.
The StateMachine class is used to perform transitions. The StateMachine class is not designed to be subclassed, it is meant to be used as is. However, for debugging, it is useful to override the toString() method to provide a meaningful name.
When an instance of StateMachine is created it is given the initial state of the machine. The state machine is started by calling the enter method which causes the initial state to be entered.
When a state is entered it first recursively ensures that its parent state has been entered. The state then invokes its associated enter action. If the state has an autoChild then the autoChild is recursively entered. The last state to be entered during this process becomes the current state. A state can have an action associated with it which is invoked when the state becomes the current state.
Transitions are then performed in two steps. First, the leaveFor method is called with an argument that is the target state of the transition. The leaveFor method exits the current state by invoking the associated leave action. It then moves up the state nesting hierarchy exiting each state until a state is encountered that is an ancestor of the target state. The leaveFor method then returns. The transition is completed by calling the enter method which causes the target state of the transition to be entered. The target state of the transition is entered using the same process described above to enter the initial state of the machine (i.e. first the parent states are entered, then the target state itself, and finally any auto children).
Each state in the state diagram is represented by an instance of the State class. If a state is nested in another state then the constructor of the nested state should take the containing parent state as an argument.
A single instance of StateMachine is used to perform the transitions in a state diagram. Pass the initial state as an argument to the StateMachine constructor.
Define the enter, leave, and "current state" actions for each state, as needed.
Parent states typically have an autoChild state, although this is not required. If a parent state should maintain its history then call setHistory(true).
A typical event handler is a public method of the class and this method represents an event in the state diagram. This is not required, however. Event handlers can assume any form required by the application.
The event handler calls isActive on each state that is interested in the event and uses the StateMachine instance to perform transitions. Guard conditions, transition actions, and internal actions are implemented using normal Java capabilities.
The body of an event handler looks something like:
if (myStateA.isActive()) { if (guardCondition()) { myMachine.leaveFor(myStateB); transitionAction(); myMachine.enter(); } } else if (myStateC.isActive()) { internalAction(); }
This is an event-oriented approach and it is possible to tell at a glance which states are interested in an event. Contrast this to the implementation given in the "Gang of Four" design patterns book: the Context delegates state-specific requests to the current state so it is easy to determine which events a state is interested in.
This package does not provide any specific support for concurrent state diagrams. If your class is modeled using concurrent state diagrams then you can create an instance of StateMachine for each concurrent state diagram but you must manually keep all the state machines in sync.
Before performing any transitions you must first call the enter method to start the state machine.
The following line would be added to the code that structures the state machine:
myStateActive.setHistory(true);
No other changes are necessary. In particular, the code that handles the "activate" event does not change even though the transition is now more dynamic.
Robert Martin has created a finite state machine compiler. Below is a state diagram of one of the subway turnstile examples that he uses. An implementation using this idiom is provided for comparison.
The implementation of a state diagram is straightforward using org.jmonde.state. Once it is implemented, however, it is not easy to "see" the state diagram in the code. This is because the different parts of the state diagram are spread throughout the code.
The source is available under the GNU General Public License.
Release 1.1.0