Introduction – Finite State Machines

An application can be thought of as a collection of states and transitions between states. Transitions should be controlled – you should only be able to proceed from one state to another via a specific functionality designed to handle the transition between those states.

A finite state machine describes and implements this collection of states and transitions. It doesn't code anything else – it doesn't build UI components or data structures – it simply transitions through the defined states on specifically defined triggers, notifying your app when a transition occurs.

To build your application using this approach entails listening to the finite state machine (fsm, for short) for transition invocations. To effect a transtion, something in your code (likely a UI operation, a timer, or task completion) calls the fsm to trigger a given transition and the machine then proceeds to the next defined state.

The outfall of using this approach is that the various classes (and components, especially) in your application are isolated. Functionality, in turn, is isolated. There are specific (finite) points of interaction, defined by the state machine. Each component (or function, if a component isn't involved) implements only the functionality necessary for the given state or transition with which it is associated.

Even if you have a multi-purpose component (this application has one), it can still behave predictably in a given state – state is well defined, and information tied to that state is discrete.

You can define broadcast transitions – for instance this application has a timeout that affects practically every state and preempts every transition. That behavior can be defined and handled cleanly in the fsm definition.

Overview

The fsm implementation in this demo is the JakesGordon/Javascript State Machine package. It's a very good implementation with the added benefit of being able to produce GraphViz diagrams of the state directly from the state definition (see below).

The demo is an amplification of the ATM example in that package. It has been fleshed out with real UI components and functions to demonstrate how a real application might be built with this technique.

The goals of the demo have been:

  • Create a more realistic demo with more realistic states and transitions:
    • Errors
    • Validation/Verify
    • Timeout
  • Exercise various ways to respond to transitions and states.
  • Make the whole thing work as a full Vue.js application.
  • Implement and test fsm composibility.
  • Implement as a progressive web app (this is a bonus: the demo is self-contained – and should run offline if the browser supports the 'service-worker' functionality.)(currently Chrome and FireFox have the best progressive web app support.)

Composed States

With even something as simple as an ATM application, representing all possible states in a single state machine definition can become quite confusing, quickly. It's extremely useful to be able to break definitions into multiple fsms, with a broadly functional overview "main" or "controller" fsm, that can then hand off control to one of potentially several 'sub-fsm' definitions, wait until that 'sub-fsm' is finished with it's task, and then resume the top-level state handling.

Ability to define 'sub-sfms' also allows repurposing – in this case, a 'transaction' fsm may be of a deposit or a withdrawal type, but the path within the fsm is essentially the same for either, with only minor variations that can be determined by the transaction type. Defining a single sub-sfm definition to handle both types reduces code proliferation, and enhances maintainability.

JG's fsm implementation does not provide for composing state definitions. However, since fsms can be instantiated independently, it's a simple matter to create them as needed, push them on a LIFO stack, and then pop them at the end of their lifecycle. This approach has been used here with good results by creating an 'fsm_manager' facility to handle the pushing and popping of fsms (it's also a good place to implement the timeout facility.)

The data-storage capability within the fsm instance can be used to store information differentiating the instances – in this case, deposit vs withdrawal transaction type. It can also be used to store information as execution proceeds from one state to the next (see the transaction sub-fsm.)

State Diagram Generation

There are several fsm implementations available for javascript – one feature that makes this implementation rise above the rest is the diagram-generation capability – creating dot files that can be ingested by GraphViz to generate the diagrams.

Throughout the development of this project, it's been extremely useful to crank out diagrams and refer to them while constructing the app, to insure the app conforms or to spot any errors or omissions. The diagram allows you to spot troubles quickly – It's then a simple matter to change the fsm definitions and crank out another set of corrected diagrams for the next iteration.

That is frankly, a killer capability – not only is it useful to drive development, but it also is a great communication tool to use with the client. Everyone knows what happens at each transition and state.

Implementation Highlights

Vue.js Implementation

The app is a Vue.js application, along with Vuex for data-store handling, and VueRouter for routing. In this app, the data-store is the customer accounts and balances (representing the bank.) Ostensibly, for an app of this nature, you wouldn't need routing, but the intent was to see how a full application with routing might be implemented with the fsm technique.

fsm_manager/atm_fsm_manager

(Update: This is now broken out into its own NPM module — fsm-manager)

The 'fsm_manager' class and app-specific-derived class 'atm_fsm_manager' are at the heart of the application, and control how the states are handled. Some states correspond to a visual component, others do not. The router is handed off to the fsm_manager constructor on instantiation, which now manages routing based on state changes.

On a state change, the manager checks the router to see if a route exists for the 'to' state (by convention, the route is named the same as the state to avoid any mapping requirements.) If it exists, the route is triggered. If not, control is handed off to 'handleStateChange()', which is expected to be implemented in the 'fsm_manager'-derived class.

Timeout Transition

An ATM would be expected to timeout after a period of inactivity. This introduces timeout transitions from almost every state. The 'atm_fsm_manager' creates and manages the timer. After 1 minute of inactivity, the app will time out and revert to the 'card-return' state. If the current fsm is the 'atm-transaction' sub-fsm, the stack is popped, and the timeout notification is passed to the 'atm-main' fsm.

(Note the timeout transition preempts any other transitions. This is easy to implenent in the state handler by checking the 'transition' value, and behaving accordingly.)

Timeout functionality might be a generally useful thing – it could be generalized into a facility as part of the 'fsm_manager' set. It would require a canonically defined (or registered?) transition – 'timeout' in this case – and a canonical piece of data contained within the fsm – 'timed-out', here.

This factoring is not complete, yet, but the underpinnings are in place.

Error Transitions

The error transitions ('transaction-invalid', and 'transaction-error') could be handled in the Vue components without triggering fsm transitions, but I wanted to do this more formally – for instance, something else might want to know that an error has occurred.

By triggering an fsm transition to another state, anything can monitor and respond to the error condition.

Leveraging Same Component for Multiple States

The 'transaction-validate', 'transaction-verify', and 'transaction-exec' states are all handled by a single component – it changes content based on the state and state-specific information within the fsm. To do this requires the component to register with the fsm to receive the state-change signal, and then retrieving the information from the current fsm when that signal happens.

Using a Modal Dialog to Respond to State

The 'confirm-cancel' state is represented by a modal dialog, rather than a routed component. Again, the intent is to exercise multiple responses to changes. By utilizing the fsm history plugin, it's a simple matter to step back if cancellation is accepted or do nothing if rejected .

Maintaining Information Between States

The transaction type, account, and amount values need to be maintained from state to state during the transaction cycle. This is another good utilization of the fsm data facility. Keeping this information here until the transaction has been completed isolates it from the data-store, committing to the store only when the transaction has been verified and accepted.

Progressive WebApp

Something of a bonus – if the browser supports it (Chrome, Firefox, currently). The demo can be run offline as a Progressive WebApp, and even installed to a phone's desktop as a native app.

Diagrams

These are the diagrams generated from the state definitions of the main and sub-state fsm definitions. The output dot files are further massaged to produce some more relevant output, highlighting the sub-state instances in the main, and then providing simplified schematic diagrams that are a bit less dense-looking.

ATM Main

This is the main fsm. 'deposit-transaction' and 'withdrawal-transation' (colored yellow) are instances of the 'atm-transaction' fsm.

atm-main state diagram

atm-main – schematic state diagram


ATM Transaction

This is the transaction sub-fsm. It is instantiated and pushed onto the fsm stack as required. At the exit, it is popped off the fsm stack and control return to the 'atm-main' fsm.

atm-transaction state diagram

atm-transaction – schematic state diagram

Proposed Changes to the fsm Package

First off, the package is a terrific implementation. Following is an discussion of perceived issues, and proposed solutions to address them.

Perceived Issues

Diagramming Could Be Enhanced

The diagramming capability is perhaps the standout feature of this package. However, there are aspects (used in this application) that aren't reflected in the diagrams that would be very helpful if they were. For instance some states correspond to visual components while others do not. Some states are instances of sub-fsms. By post massaging the dot output, it's possible to do a bit, but more could be build into the facility.

State Definitions Are AdHoc

In the current implementation, one doesn't define states but rather transitions. States are defined as an aspect of the transition. States then, are something of a byproduct. It would be better (and less prone to error entering states) if states were defined up front. Then, if a non-existent state was entered in the transition, the facility would catch that and throw an error.

No Built-In Composibility

In the documentation, the package author indicates this is on the list of todos.

Debugging Requires Stepping Through Each State

This can be tedious. It would be beneficial to be able to go to a state directly during development. It would also enhance testing.

Proposed Solutions

Require (or Allow) State Definitions Up Front

This would consist of a list of state names preceding the transition definition list. Doing so would enable:

  • The transition definition mechanism to catch any undefined states.
  • dot directives to be associated with the states (much the same way they are associated with transitions, now. Useful directives might be initially:
    • Shape
    • Fill color
    • Text information (GraphViz supports hover tooltips.)

The feature could be optional, if you didn't want to break current functionality. The package would simply check to see if a state definition list exists. If it does, implement the checks -- if not, proceed as before.


One Approach to Composibility — 'fsm_manager'

Adding the concept of 'fsm_manager' makes composibility possible. It could be added to the package.

Provide Way of Proceeding Directly to A State

There may be a way of doing this, already – I haven't investigated. It's somewhat complicated by the fact that data may be required to go to a given state (and would have to be implanted in the fsm before triggering.) Sub-fsm states would add further complication.

Have to think on this some more. One approach might be to record state changes (and fsm data changes) and store that recording in some form. Then, reset original state and play back the changes – unhooking the UI until the last step. That would relieve the necessity of setting up states in a fixture of some sort.