User Interfaces You Can Trust with State Machines

Building robust, maintainable, testable user interfaces with state machines

bricoi
bricoi
May 23 · 14 min read

The movie search app

Detailed specifications

Your preliminary analysis produced detailed specifications and a set of screens corresponding to different stages of the user flow.

Implementation

The TDD methodology leads to an implementation of the movie search application:

  • the controller elicits what actions to do, based on the current value of a model
  • the controller performs those actions, and updates the model

Refactoring towards state machines

Did you notice the shape of our specifications? Abstracting over application-specific content, the specifications follow the pattern: GIVEN state WHEN event THEN actions. That is what I termed as the event-state-action paradigm, which can be used to describe most of the user interfaces on the web. This paradigm leads us to a refactoring with state machines.

Event-state-action paradigm

The (GIVEN, WHEN, THEN) BDD triples can be written formulaically as actions = f(state, event). We will call f the reactive function associated to the behaviour. In any of the following equations, keep in mind that any function mentioned is a mathematical function, which can be implemented programmatically by means of a pure function without side effects. Here is a partial mapping for one such function:

State machine formalism

There are thus many ways to implement the reactive function f. State machines (more precisely Mealy machines or state transducers) are a way to write the reactive function, so that it is amenable to formal reasoning, and visualization. The state machine achieves this flexibility by separating the pieces of state which are involved in control (referred to as control states) from the rest of the application state. Here is an alternative event-state-action mapping for our movie search application:

  • control states
  • an extended state variable
  • transitions between control states, mapped to actions to be executed as a result of an event triggering the transition

Why use state machines

Incorporating state machines early in your development process may bring the following benefits:

  • reduce implementation bugs
  • iterate on features faster and more reliably
  • have an automatable and clear documentation of the interface for all members of the development team
  • pick and change front-end architecture as the need emerges

Identify design bugs early

Let’s get back to our TDD implementation. The event-state-action mapping realized in that implementation can be represented by the following state machine:

Identify and reduce implementation bugs

A state machine modelization may lead to reduced bugs at implementation time for two reasons:

  • the testing of the state machine can be automated.

Automatic code generation

It is fairly easy to write a state machine by hand. The following state machine (replicating behavior of a Promise):

Automatic test generation

We have seen that an alternative formulation g of the reactive function f which is oriented to implementation. It is also possible to derive an equivalent, pure function h such that actionsSequence = h(eventSequence). The benefit of that formulation: there is no longer an opaque internal state, so we can use that formulation for testing purposes, simply by feeding event sequences into the reactive function.

Iterate on features

After experimenting with the prototype, the UX could be improved with a few changes:

  • debounce input
  • query the movie database only after three characters get entered
  • viewing movies may require being logged in depending on the rating (18+ etc.)
  • movie details could have its own route, for linkability purposes

Clearly and economically document the interface behavior

Arguably the state machine for promises we visualized earlier is a useful mechanism to explain promise behavior. State machines get visualized in different ways, emphasizing different pieces of information. To discuss this approach with designers, it is possible to focus the visualization on control states and transitions. With developers, it may be preferred to include technical details such as internal state updates. For quality assurance purposes, some paths in the state machine can get emphasized (core path, error paths, etc.).

Fits any front-end architecture

The machine completely isolates the behavior of the user interface from other concerns, and controls the other relevant pieces of the front-end architecture through a command pattern. This has non-trivial architectural implications: the choice of a rendering engine can be reversed without modifying the machine (the behavior has not changed!); libraries to execute effects can also be swapped out at will at any point of time (e.g. fetch-jsonp may be replaced by window.fetch).

The purpose of a good architecture is to defer decisions, delay decisions. The job of an architect is not to make decisions, the job of an architect is to build a structure that allows decisions to be delayed as long as possible. Why? Because when you delay a decision, you have more information when it comes time to make it.

To illustrate the point, here are implementations (Vue, React, Ivi, Inferno, Nerv, Svelte, Dojo) of the online movie search app among 7 front-end frameworks with diverse characteristics, with the exact same state machine:

Final state machine implementation

You can have a look at a final implementation with all fixes of the online interface to the movie database using dedicated state machine libraries.

Conclusion

Modelling user interfaces’ behavior with explicit state machines produces robust and maintainable interfaces. That is the reason behind their success in safety-critical software for embedded systems (nuclear plants. air flight systems, etc.). Additionally, it allows engineers to reason easily about, and update, complex behaviors. That is why the technique is popular in modelization of the complex behavior of game agents. The automatic test and code generation can also translate in improved productivity of the development process (less debugging, less boilerplate code to create).

DailyJS

JavaScript news and opinion.

bricoi

Written by

bricoi

DailyJS

DailyJS

JavaScript news and opinion.