Does your code actually reflect the state of your system? Flags vs Enums vs State Machines

TL;DR: Using flags to control the state of your system can become messy really quickly. Try using a state machine (or at least an enum) to overcome this 🔥

Let's face it, if you have been creating software for some time, you may have encountered code that looks like this:

if (isLoading) {
  // Do some stuff that reflects a loading state
} else if (error) {
  // Do some stuff that reflects an error state
} else {
  // Do some stuff that reflects a success state
}

And you may think:

Hey, that isn't so bad… the code is working dude 😉

And I would agree with you… but is it really reflecting the state of your system?

  • What does it mean when the isLoading flag is "false" and you have no error? Does it mean that you got a successful response? Are you sure you did not forget to actually trigger the request?
  • What does "error" really mean? Does it mean that you got an empty response? What if the event was cancelled, does error mean that as well?
  • We can continue with this list… but I think I made my point 😄

Alright, let's try to address those concerns. In the code above, how can we know if we actually triggered the request? The easiest solution (and the most commonly used) is to add a new flag that represents if the event has been triggered. I'll call this new flag hasFired.

With a new flag in the game, some questions arise: where should I put the new flag? should I use it in conjunction with the other flags? does it need to be in its own if statement? Let's see the first case…

if (hasFired && isLoading) {
  // Do some stuff that reflects a loading state
} else if (error) {
  // Do some stuff that reflects an error state
} else {
  // Do some stuff that reflects a success state
}

What happened here? It "looks like" we solved our concern… but not quiet right. Now, if we have not started the event and we have no error, the code will go to the else statement, reflecting the success state, but that was not what we were looking for…

How can we fix this? do we need an extra flag for the success state? should we use the new hasFired flag in all the other if/else statements as well? Yeah, I don't think this is a good way to go…

Now, let's try to go the other way around and use the flag individually:

if (!hasFired) {
  // Okay, we haven't started the event...
} else if (isLoading) {
  // Do some stuff that reflects a loading state
} else if (error) {
  // Do some stuff that reflects an error state
} else {
  // Do some stuff that reflects a success state
}

This seems to be working right? we are now saying that if we haven't triggered the event, we won't continue with the validation flow, which seems to be addressing our first problem. Notice how I had to put the validation at the beginning, because this seems to be the step #1 in our desired flow. Hold that thought for now and let's continue.

Depending on how you create that new flag, the implementation can defer a little bit, but you will be ending with something like that.

But anyway, I don't know about you, but I think that even though this is working, it is starting to look weird and the complexity seems to be growing anyway…

Indeed the complexity is growing. As a matter of fact, whenever you use a new flag, you are introducing two new possible flows to your code (when the flag is truthy and when is falsy). For each flag that you add to your system, you are actually increasing the number of flows by a factor of two; therefore: 1 flag → 2 flows, 2 flags → 2 * 2 flows, 3 flags → 2 * 2 * 2 flows … Then, we can express that a flag increases the complexity of the system by a factor of 2x, where "x" is the number of flags in your system.

As you can see, we were able to fix our problem, but at what cost? We had to introduce a new variable to our system. We had to think and evaluate where and how to use that new variable. If done incorrectly, we could have introduced new bugs.

This doesn't seem to escalate very well, what if we wanted to address our second problem? For example, having a "cancelled" state. Would you introduce a new variable to your system? Where and how would you use it? Are you willing to repeat this same process for each new use case that appears within your system?

C'mon, there has to be a better way!

Introducing Enums

For those who don't know, an enum is like a "data type" that enables a variable to only have a value that exists in a list of predefined constants.

I know I know, maybe a code example would be better. There are different ways to implement an enum depending on the language that you are using and so on, to keep it simple, I will use a JavaScript object to illustrate our example:

const myStates = {
  LOADING: 'loading',
  ERROR: 'error',
  SUCCESS: 'success',
}

As you can see, we define a list of possible values that a variable can use to represent something.

Now that we have a new tool in our arsenal… can you think in a way to use an enum in order to fix the code that we have been struggle with? What if, instead of using all of those different flags, we use just one enum variable?

Let's try it! I'm going to name this enum as status to replace all of our old variables:

if (status === myStates.LOADING) {
  // Do some stuff that reflects a loading state
} else if (status === myStates.ERROR) {
  // Do some stuff that reflects an error state?
} else {
  // Do some stuff that reflects a success state
}

Nice! Code starts looking better! 🎉

Now that we have replaced the flags in our original code, how can we address the concern of knowing if we actually triggered the event? The answer would be, to add a new constant to our enum!

Something like:

const myStates = {
  IDLE: 'idle',
  LOADING: 'loading',
  ERROR: 'error',
  SUCCESS: 'success',
}

Where idle would be a new state in our system that means that we have not done anything yet.

Now, remember how did we solve this same issue using flags? We had to create a new flag and then place it in a new if statement.

Let's do the same thing but with our enum instead:

if (status === myStates.IDLE) {
  // Okay, we haven't started the event...
} else if (status === myStates.LOADING) {
  // Do some stuff that reflects a loading state
} else if (status === myStates.ERROR) {
  // Do some stuff that reflects an error state
} else {
  // Do some stuff that reflects a success state
}

Cool!

This looks a lot better than our old implementation. And not only that! We have decreased the number of variables and, therefore, the complexity of our system 👏🏻

Now, let's try to address the other proposed concern, having a cancelled state. If you were paying attention, you may already know how to solve this in an easy way.

Let's create a new constant in our enum and then use it!

const myStates = {
  IDLE: 'idle',
  LOADING: 'loading',
  CANCELLED: 'cancelled',
  ERROR: 'error',
  SUCCESS: 'success',
}

// ...

if (status === myStates.IDLE) {
  // Okay, we haven't started the event...
} else if (status === myStates.LOADING) {
  // Do some stuff that reflects a loading state
} else if (status === myStates.CANCELLED) {
  // Do some stuff that reflects a cancelled state
} else if (status === myStates.ERROR) {
  // Do some stuff that reflects an error state
} else {
  // Do some stuff that reflects a success state
}

Pretty easy right?

As you can see, enums can help you to easily define a set of constants that can reflect the intention of an action. Here we saw one of the most common examples, which is triggering an event that can take some time to complete; but you can use this technique in a lot of different problems! For example, you can use it to represent toggles, animations, form steps, etc.

This solution works really well in most cases; however, I would like to introduce you to a new problem.

What if we want to introduce a safe mechanism to change from one state to another? Like adding rules to every state change?

If you really think about it, in our example with enums you can be on an idle state and go directly to an error or success state, without even going to the loading state before; there is nothing that will prevent you of doing that. Of course you can manually add some validations for this task, but I can guarantee you that it will become really messy quickly.

To address this problem, let's take a look to a new concept that we have not talked about yet…

State Machines

If you look up this term, you may find a lot of (probably scary) definitions for state machines, you can even find different types of machines, but for now we will be working with Finite State Machines.

In simple words, a finite state machine is a model for determining in which state something can be in. For example, some electro domestics can be either on or off, they can never be both at the same time; and in order to be on it needs to be off before, and vice versa. Of course this doesn't work in a boolean fashion only, you can use it to represent any number of states for a given problem.

One interesting thing when working with finite state machines is that in order for you to change from one state to another, you need to declare a transition between them. That transition is basically an event that tells the machine that the current state needs to be changed.

Alright, alright. Let's explain this with a simple scenario. Imagine that you have two states, off and on, and the way to change from one state to the other is by clicking a button. We can represent this scenario with a simple diagram that looks like this:

DoesYourCodeReflectTheStateOfYourSystem/Screen_Shot_2020-06-04_at_22.35.04.png

Notice how I specified "Off" as the initial state. A finite state machine needs to know where to start!

We can clearly see that when the machine is in the off state, it needs the button clicked action in order to go to the on state. And when the machine is in the on state, it needs the same button clicked action in order to go to the off state. It's important to mention that those actions don't need to be the same necessarily, I just used the same to make things easier.

Now that we have a basic understanding of finite state machines, do you remember that we had an issue with our enums solution because we could change from one state to another without something preventing us for doing so?

Let's try to address that. Step number one will be to create a complete diagram for our scenario:

DoesYourCodeReflectTheStateOfYourSystem/Screen_Shot_2020-06-04_at_22.54.08.png

As you can see, we have modeled all the possible states and how can we transition from one state to another. Therefore, we have eliminated the case where you could go from idle state directly to error or success state.

If the machine is in an idle state, it firstly needs to detect a fetch request action in order to change to the loading state; then, depending on which event is detected, the machine will transition to the desired state, either success, error, or cancelled.

Perfect! but… how can we code this? 🤔

Turns out it is easier than what you may be thinking…

Let's model our states and transitions, you can do this in different ways, to keep it simple I will use a simple JavaScript object:

const fetcherMachine = {
  initialState: "idle",
  states: {
    idle: {
      transitions: {
        fetch: "loading",
      },
    },
    loading: {
      transitions: {
        resolved: "success",
        rejected: "error",
      },
    },
    success: {},
    error: {},
  },
}

Cool, but we are not done yet!

We need to create the mechanism that will have the control of our machine, because we don't want to use that object directly!

I will create a class named StateMachineInterpreter. This class will receive a machine and will use a private property to reflect the current state of the machine, which at the beginning will be the initial state that we have defined:

class StateMachineInterpreter {
    constructor(machine) {
    this.machine = machine
    this.currentState = machine.initialState
  }

  getState() {
    return this.currentState
  }
}

Notice how I also added a getState function that the consumer of this class will be able to use to know the current state value. We don't want the consumers to be able to access the current state directly.

Now, we need to add the logic to change the state. I will create a function called transition that will be in charge of modifying the current state given a transition name.

class StateMachineInterpreter {
  constructor(machine) {
    this.machine = machine
    this.currentState = machine.initialState
  }

  getState() {
    return this.currentState
  }

  transition(transitionName) {
    // First, we validate if the current state has any
    // transitions available.
    if (!this.machine.states[this.currentState].transitions) {
      return this.currentState
    }

    // We try to get the new state, applying the transition to
    // the current state.
    const nextState = this.machine.states[this.currentState].transitions[
      transitionName
    ]

    // It could be the case where the current state did not have
    // a transition with the same name as the argument.
    // We need to check if we were able to find a next state before
    // assigning that new state to the current one.
    if (nextState) {
      this.currentState = nextState
    }

    return this.currentState
  }
}

Guess what? we are done 😄

That is a really basic implementation of a finite state machine. Let's see in action our new class and the fetcherMachine object that we defined before:

// We initialize our interpreter
const fetcherMachineInterpreter = new StateMachineInterpreter(fetcherMachine);

// Will return "idle" as it's the initial state of the fetcher machine.
fetcherMachineInterpreter.getState()

// Will return "loading" as the new state, because "idle"
// has a transition named "fetch" that points to "loading" state.
fetcherMachineInterpreter.transition("fetch")

// Will return "success".
fetcherMachineInterpreter.transition("resolved")

Cool 🔥

Okay, do you remember our last example with enums? We can now use our new machine there as well:

const state = fetcherMachineInterpreter.getState()
if (state === myStates.IDLE) {
  // Okay, we haven't started the event...
} else if (state === myStates.LOADING) {
  // Do some stuff that reflects a loading state
} else if (state === myStates.CANCELLED) {
  // Do some stuff that reflects a cancelled state
} else if (state === myStates.ERROR) {
    // Do some stuff that reflects an error state
} else {
    // Do some stuff that reflects a success state
}

And there you go, now the state of your system is being correctly reflected 🎉. Also, adding new states and transitions to your code has become a really easy task to do.

Some of you may be thinking that we still have a lot of if statements, but that is a problem for another day 😄

Conclusions

State Machines are a powerful tool that can help you create better quality code, because they reflect the actual state of your system.

I just described a really basic use case for them, but you can use state machines to have more robust implementations, like triggering events whenever the state changes, and so on.

You can also use already made-up solutions like XState for JavaScript, and I'm sure you can find implementations for other languages as well.

Happy Coding! 🔥

* Bonus *

You could benefit a lot from state machines when using typed languages, making the machine fully deterministic without any surprises!

Here is an example on how the code described in this blog can be done with TypeScript:

type StateMachine<T extends string> = {
  initialState: T;
  states: {
    [keys in T]: {
      transitions?: {
        [key: string]: T;
      };
    };
  };
}

// ...

class StateMachineInterpreter<T extends string> {
  private currentState: T

  public constructor(private machine: StateMachine<T>) {
    this.currentState = machine.initialState
  }

  public transition(transitionName: string): T {
    if (!this.currentStateHasTransitions()) {
      return this.currentState
    }

    const nextState = this.machine.states[this.currentState].transitions[
      transitionName
    ]

    if (nextState) {
      this.currentState = nextState
    }

    return this.currentState
  }

  private currentStateHasTransitions(): boolean {
    return Boolean(this.machine.states[this.currentState].transitions)
  }

  public getState(): T {
    return this.currentState
  }
}

// ...

type FetcherStates = "idle" | "loading" | "resolved" | "rejected"

const fetcherMachine: StateMachine<FetcherStates> = {
  initialState: "idle",
  states: {
    idle: {
      transitions: {
        fetch: "loading",
      },
    },
    loading: {
      transitions: {
        success: "resolved",
        error: "rejected",
      },
    },
    resolved: {},
    rejected: {},
  },
}

// ...

const fetcherMachineInterpreter = new StateMachineInterpreter(fetcherMachine)
console.log(fetcherMachineInterpreter.getState())
console.log(fetcherMachineInterpreter.transition("fetch"))
console.log(fetcherMachineInterpreter.transition("success"))

🔥