Designing finite state machines requires good discipline and a shift in design paradigm. It’s very easy to “break” a state machine by implementing improper design. It is very tempting to take shortcuts, but it never leads to clean lasting code. In this post, I want to examine (rant on) some tempting pitfalls.

The basic premise for an FSM is: each state updates and states run one at a time, each state has enter and exit logic and has one or more switch conditions to different state(s).

The examples are very simple, just a state enum and a state variable. But the associated issues could equally appear in a similar form in a more complex implementation. Even simple FSM frameworks cannot prevent abuse without unnecessarily restrictive or cumbersome design.

Conditions in State Updates

One of the biggest grievances of state update methods is when they grow new conditions in them:

becomes

This is another state. There should never be branching conditions in a state machine that toggle the state’s actions. If state’s actions are conditional, then those are–by definition–different states. This can also appear as simply as:

Pausable States

Let’s say some state can be suspended:

Again, that’s another state. There is no such thing as “pausing” a state. A paused state is a different state whose logic is to wait until and resume the original state. Pausing a state is the same as adding a condition where one branch is “do nothing”.

External State Changes

While writing a state machine that reacts to external conditions, it’s tempting to simply tell the state machine to change. Even if this method is part of the state machine and the actual state and value are not exposed:

Firstly, this is still external from state machine’s point of view. This bypasses any state switch conditions or logic and introduces a second mechanism to switch states. What the proper state-respectful and error-tolerant way to do this is to set the “desired state” flag:

and somewhere later, when one or more state switch are considered, check this flag in addition to any normal conditions (and reset it when the state is finally switched):

The state value should always be read-only to anyone but the state machine’s state switch code. In other words, there would be only one place in code that assigns the state value.

Manual State Update Code

Each state should run at and only at its designated update time. For a simple example, the essential logic is this:

It is possible to write logic for the states while completely ignoring any state logic separation:

In small doses, this may even make sense (formatted nicely, it looks like good code). But this really just throws the state design out the window. This code cannot be refactored. The whole point of states are to be independent.

Reusing State Values

The simplest state machine is just an enum:

Since it’s an enum, you could manually assign underlying values to it:

And then actually use them for other purposes:

This is breaking single responsibility principle. This is bad because neither the states nor the values can be refactored without affecting the other usage. This is also very forced. Variations include things like label[(int)state].Show() or  animator.state = (int)state;.

Enumerated States

States can require additional persistent data. It could in some cases be encoded in the states themselves:

This has 5 “Floor” states that do exactly the same logic, but for different floor numbers. This mainly breaks “don’t repeat yourself” principle and makes adding new states of changing numbering cumbersome. This should likely be closer to:

It’s still a bit ugly. Depending on how it’s used and the way the state machine is implemented, there are many ways such data can be stored (that I won’t go into).

Modifying State Data

States can have data and–unless we are using a fancy encapsulated framework–this data could likely be just another variable:

It’s important to not touch this variable outside the designated state (and rarely multiple states). It might be tempting to do something like:

This makes assumption about the current state and state specifics. It should be the state that handles this at the appropriate time (if necessary, there should be a separate variable for “next bonus”):

Unknown Initial State

I haven’t added an initial state, such as “None”, to my examples above. In a real system, that can be a dangerous assumption if the system has persistent data and could halt and restart unexpectedly. Consider a forklift:

We may expect the lift to always lower when it is powered off, but what if the power cuts off? In other words, the state machine has to know which state to enter first. It might be tempting to simply add some initial conditions:

Besides assigning state directly, this is also making assumptions about how a state switches/starts. The code is now duplicated for state entry and awaiting many hours of debugging.

Final Thoughts

A lot of issues can be solved simply by using an existing framework. Most frameworks would enforce proper design. Unfortunately, more complex frameworks also have downsides, such as learning curve, reduced performance and increased memory usage or many new classes. Many of these downsides come into play when the application has dozens if not hundreds of state machines. I often find myself needing a quick state machine in a single class and making half a dozen extra classes for a full-blown state machine is hard to justify. So I would use a simple state machine pattern, but one open to abuse without discipline.

State machines are alive and kicking. As with any design pattern, knowing its strengths and weaknesses let’s you choose and use it appropriately. When used correctly, it is a powerful tool that can divide complex timed systems into manageable chunks. FSM is still one of my favorite design patterns.

Don’t Break the State Machine!
Tagged on:         

One thought on “Don’t Break the State Machine!

  • November 7, 2016 at 18:14
    Permalink

    Good analysis on state machines. I still tend to use the classic State Pattern (Gang of Four) for more heavy-duty state machines, but a low-maintenance implementation can definitely help organize complex logic.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *