Implement IoT State Machine Transition By State Pattern In TypeScript

eye-catch JavaScript/TypeScript

In my project, my team needed to implement State machine. One of the developers implemented it by using many if-else but I thought it could be a good example to apply/learn State pattern. So I privately tried to implement it with State pattern.

Let’s compare the two implementations.

Sponsored links

The provided state model

The target state machine model is the following.

We need to get the current state first, e.g. NotSelected, Idle, Running… And then, event name (Selected, ProgramStarted, Stopped…) is sent by the machine. When our software receives event name, we need to update the current state according to the state machine model.

How do you implement this?

Sponsored links

One class that handles the all cases

The easiest way is to implement all transitions in a single class. It looks like the following.

enum and the UnexpectedEventError are defined in different files.

import { MachineEvent, State } from "./StateDefs";
import { UnexpectedEventError } from "./UnexpectedError";

export class StateMachine {
    public get currentState(): State {
        return this._currentState;
    }

    constructor(private _currentState: State) { }
    public changeState(event: MachineEvent) {
        let newState: State | undefined;

        switch (this._currentState) {
            case State.NotSelected:
                if (event === MachineEvent.Selected) {
                    newState = State.Idle;
                }
                break;
            case State.Idle:
                if (event === MachineEvent.SelectCleared) {
                    newState = State.NotSelected;
                } else if (event === MachineEvent.ProgramStarted) {
                    newState = State.Running;
                }
                break;
            case State.Running:
                if (event === MachineEvent.Stopped) {
                    newState = State.Stopped;
                } else if (event === MachineEvent.Interrupted) {
                    newState = State.Interrupted;
                } else if (event === MachineEvent.Finished) {
                    newState = State.Finished;
                } else if (event === MachineEvent.Error) {
                    newState = State.Error;
                }
                break;
            case State.Stopped:
                if (event === MachineEvent.ProgramCanceled) {
                    newState = State.Idle;
                } else if (event === MachineEvent.Started) {
                    newState = State.Running;
                }
                break;
            case State.Interrupted:
                if (event === MachineEvent.ProgramCanceled) {
                    newState = State.Idle;
                } else if (event === MachineEvent.Error) {
                    newState = State.Error;
                } else if (event === MachineEvent.Started) {
                    newState = State.Running;
                }
                break;
            case State.Error:
                if (event === MachineEvent.ErrorCleared) {
                    newState = State.Interrupted;
                } else if (event === MachineEvent.ProgramCanceled) {
                    newState = State.Idle;
                }
                break;
            case State.Finished:
                if (event === MachineEvent.ProgramCanceled) {
                    newState = State.Idle;
                } else if (event === MachineEvent.ProgramCompleted) {
                    newState = State.Idle;
                } else if (event === MachineEvent.Error) {
                    newState = State.Error;
                }
                break;
            default: break;
        }

        if (newState === undefined) {
            throw new UnexpectedEventError();
        }

        this._currentState = newState;
    }
}

This is simple and I guess most people tend to implement it in this way for the first time.

It is ok to implement it in this way because it doesn’t have any other methods that need to check the current state. If we need to add another method that needs to check the current state, we need to implement the switch-case or if-else in the new method too.

It means that the code will be duplicated.

Apply State Pattern to State Machine

I like State pattern and Strategy pattern in the design patterns of GoF. I often apply the pattern in my project. The code above will have tight coupling to the client because the client has to handle the class directly. It’s better to have low coupling.

Let’s decrease the coupling.

All operations are done through an interface

Firstly, we need to define an interface to reduce the coupling. The goal of the structure is the following.

Context is the client to use the MachineState. It wants to have the current state. The current state changes when a new event comes. Therefore, the interface has currentState getter and changeState method that requires an event.

The interface will look like this in TypeScript.

export interface MachineState {
    readonly currentState: State;
    changeState(event: MachineEvent): MachineState;
}

The concrete implementation must be a class that implements the interface. Each class has its own state, so we can focus on the one state to implement the transition because other transition logic is written in a different class.

This means that we need to create as many classes as the number of states. I’ve read “State pattern is not scalable” in Stack Overflow if we need additional 10 states in the future. However, those additional state transitions must be implemented somewhere. If we don’t apply State pattern, the transition logic will be added to a single class.

I think the state pattern is useful if the class has more than 2 methods that need to check the current state in both methods.

Concrete implementation is written in a class

The concrete implementation must be written in a separate class. The implementation looks like this below.

export class NotSelected implements MachineState {
    public readonly currentState = State.NotSelected;

    public changeState(event: MachineEvent): MachineState {
        if (event !== MachineEvent.Selected) {
            throw new UnexpectedEventError();
        }
        return new Idle();
    }
}

export class Idle implements MachineState {
    public readonly currentState = State.Idle;

    public changeState(event: MachineEvent): MachineState {
        switch (event) {
            case MachineEvent.ProgramStarted: return new Running();
            case MachineEvent.SelectCleared: return new NotSelected();
            default:
                throw new UnexpectedEventError();
        }
    }
}

export class Running implements MachineState {
    public readonly currentState = State.Running;

    public changeState(event: MachineEvent): MachineState {
        switch (event) {
            case MachineEvent.Stopped: return new Stopped();
            case MachineEvent.Interrupted: return new Interrupted();
            case MachineEvent.Error: return new ErrorState();
            case MachineEvent.Finished: return new Finished();
            default:
                throw new UnexpectedEventError();
        }
    }
}

export class Stopped implements MachineState {
    public readonly currentState = State.Stopped;

    public changeState(event: MachineEvent): MachineState {
        switch (event) {
            case MachineEvent.Started: return new Running();
            case MachineEvent.ProgramCanceled: return new Idle();
            default:
                throw new UnexpectedEventError();
        }
    }
}

export class Interrupted implements MachineState {
    public readonly currentState = State.Interrupted;

    public changeState(event: MachineEvent): MachineState {
        switch (event) {
            case MachineEvent.Started: return new Running();
            case MachineEvent.Error: return new ErrorState();
            case MachineEvent.ProgramCanceled: return new Idle();
            default:
                throw new UnexpectedEventError();
        }
    }
}

export class Finished implements MachineState {
    public readonly currentState = State.Finished;

    public changeState(event: MachineEvent): MachineState {
        switch (event) {
            case MachineEvent.ProgramCompleted: return new Idle();
            case MachineEvent.ProgramCanceled: return new Idle();
            case MachineEvent.Error: return new ErrorState();
            default:
                throw new UnexpectedEventError();
        }
    }
}

export class ErrorState implements MachineState {
    public readonly currentState = State.Error;

    public changeState(event: MachineEvent): MachineState {
        switch (event) {
            case MachineEvent.ErrorCleared: return new Interrupted();
            case MachineEvent.ProgramCanceled: return new Idle();
            default:
                throw new UnexpectedEventError();
        }
    }
}

If it receives an unexpected event, it throws an error. We don’t have to check the current state because each class represents the state.

But this state machine needs to check the incoming event type for the transition. Therefore, switch-case is still necessary.

Execution Result

export class Context {
    constructor(private machineState: MachineState) {
        this.machineState = machineState;
    }
    public changeState(event: MachineEvent): void {
        try {
            this.machineState = this.machineState.changeState(event);
            console.log(State[this.machineState.currentState]);
        } catch (e) {
            console.log(`!!! Error !!! ${e}`);
        }
    }
}

const context = new Context(new NotSelected());

context.changeState(MachineEvent.Selected);
context.changeState(MachineEvent.ProgramStarted);
context.changeState(MachineEvent.Stopped);
context.changeState(MachineEvent.Started);
context.changeState(MachineEvent.Interrupted);
context.changeState(MachineEvent.ProgramCanceled);
context.changeState(MachineEvent.SelectCleared);
context.changeState(MachineEvent.Selected);
context.changeState(MachineEvent.ProgramStarted);
context.changeState(MachineEvent.Error);
context.changeState(MachineEvent.ErrorCleared);
context.changeState(MachineEvent.Started);
context.changeState(MachineEvent.Finished);
context.changeState(MachineEvent.ProgramCompleted);

// Idle
// Running
// Stopped
// Running
// Interrupted
// Idle
// NotSelected
// Idle
// Running
// Error
// Interrupted
// Running
// Finished
// Idle

It handles the event correctly.

Adding a transition state to the code

When we need to implement not only the current state but also the transition state, how can we implement it in State pattern?

Interface definition

I defined the following interface.

Basically, just added transition variable in the same interface but I wanted to have the original code, so I defined it as another interface. changeState method is basically the same but I needed to change the data type because of the reason above.

export interface MachineStateWithTransition extends MachineState {
    readonly transition: Transition | null;
    changeState(event: MachineEvent): MachineStateWithTransition;
}

Let’s say, the interface above is the same as the following.

export interface MachineState {
    readonly currentState: State;
    readonly transition: Transition | null;
    changeState(event: MachineEvent): MachineState;
}

I just changed the interface name for the comparison.

Concrete class implementation for the transition

We need to assign one of the following values. It has the all transitions.

export enum Transition {
    NotSelected_Idle,
    Idle_NotSelected,
    Idle_Running,
    Running_Stopped,
    Running_Interrupted,
    Running_Error,
    Running_Finished,
    Stopped_Idle,
    Stopped_Running,
    Interrupted_Running,
    Interrupted_Error,
    Error_Interrupted,
    Error_Idle,
    Finished_Idle,
    Finished_Error,
}

To determine the value, the class has to know the last state. The last state is passed to the constructor and then, we can determine which value to assign.

export class NotSelectedWithTransition implements MachineStateWithTransition {
    public readonly currentState = State.NotSelected;
    public readonly transition: Transition | null;

    constructor(state?: State) {
        this.transition = state === State.Idle ? Transition.Idle_NotSelected : null;
    }

    public changeState(event: MachineEvent): MachineStateWithTransition {
        if (event !== MachineEvent.Selected) {
            throw new UnexpectedEventError();
        }
        return new IdleWithTransition(State.NotSelected);
    }
}

When the application starts, last state doesn’t exist. In this case, transition is null.

The other classes have the same structure.

export class IdleWithTransition implements MachineStateWithTransition {
    public readonly currentState = State.Idle;
    public readonly transition: Transition | null;

    constructor(state?: State) {
        switch (state) {
            case State.Interrupted:
                this.transition = Transition.Interrupted_Idle;
                return;
                case State.NotSelected:
                this.transition = Transition.NotSelected_Idle;
                return;
            case State.Stopped:
                this.transition = Transition.Stopped_Idle;
                return;
            case State.Finished:
                this.transition = Transition.Finished_Idle;
                return;
            case State.Error:
                this.transition = Transition.Error_Idle;
                return;
            default:
                this.transition = null;

        }
    }

    public changeState(event: MachineEvent): MachineStateWithTransition {
        switch (event) {
            case MachineEvent.ProgramStarted: return new RunningWithTransition(State.Idle);
            case MachineEvent.SelectCleared: return new NotSelectedWithTransition(State.Idle);
            default:
                throw new UnexpectedEventError();
        }
    }
}

export class RunningWithTransition implements MachineStateWithTransition {
    public readonly currentState = State.Running;
    public readonly transition: Transition | null;

    constructor(state?: State) {
        switch (state) {
            case State.Idle:
                this.transition = Transition.Idle_Running;
                return;
            case State.Stopped:
                this.transition = Transition.Stopped_Running;
                return;
            case State.Interrupted:
                this.transition = Transition.Interrupted_Running;
                return;
            default: this.transition = null;
        }
    }

    public changeState(event: MachineEvent): MachineStateWithTransition {
        switch (event) {
            case MachineEvent.Stopped: return new StoppedWithTransition(State.Running);
            case MachineEvent.Interrupted: return new InterruptedWithTransition(State.Running);
            case MachineEvent.Error: return new ErrorStateWithTransition(State.Running);
            case MachineEvent.Finished: return new FinishedWithTransition(State.Running);
            default:
                throw new UnexpectedEventError();
        }
    }
}

export class StoppedWithTransition implements MachineStateWithTransition {
    public readonly currentState = State.Stopped;
    public readonly transition: Transition | null;

    constructor(state?: State) {
        this.transition = state === State.Running ? Transition.Running_Stopped : null;
    }

    public changeState(event: MachineEvent): MachineStateWithTransition {
        switch (event) {
            case MachineEvent.Started: return new RunningWithTransition(State.Stopped);
            case MachineEvent.ProgramCanceled: return new IdleWithTransition(State.Stopped);
            default:
                throw new UnexpectedEventError();
        }
    }
}

export class InterruptedWithTransition implements MachineStateWithTransition {
    public readonly currentState = State.Interrupted;
    public readonly transition: Transition | null;

    constructor(state?: State) {
        switch (state) {
            case State.Running:
                this.transition = Transition.Running_Interrupted;
                return;
            case State.Error:
                this.transition = Transition.Error_Interrupted;
                return;
            default:
                this.transition = null;
        }
    }

    public changeState(event: MachineEvent): MachineStateWithTransition {
        switch (event) {
            case MachineEvent.Started: return new RunningWithTransition(State.Interrupted);
            case MachineEvent.Error: return new ErrorStateWithTransition(State.Interrupted);
            case MachineEvent.ProgramCanceled: return new IdleWithTransition(State.Interrupted);
            default:
                throw new UnexpectedEventError();
        }
    }
}

export class FinishedWithTransition implements MachineStateWithTransition {
    public readonly currentState = State.Finished;
    public readonly transition: Transition | null;

    constructor(state?: State) {
        this.transition = state === State.Running ? Transition.Running_Finished : null;
    }

    public changeState(event: MachineEvent): MachineStateWithTransition {
        switch (event) {
            case MachineEvent.ProgramCompleted: return new IdleWithTransition(State.Finished);
            case MachineEvent.ProgramCanceled: return new IdleWithTransition(State.Finished);
            case MachineEvent.Error: return new ErrorStateWithTransition(State.Finished);
            default:
                throw new UnexpectedEventError();
        }
    }
}

export class ErrorStateWithTransition implements MachineStateWithTransition {
    public readonly currentState = State.Error;
    public readonly transition: Transition | null;

    constructor(state?: State) {
        switch (state) {
            case State.Running:
                this.transition = Transition.Running_Error;
                return;
            case State.Interrupted:
                this.transition = Transition.Interrupted_Error;
                return;
            case State.Finished:
                this.transition = Transition.Finished_Error;
                return;
            default:
                this.transition = null;
        }
    }

    public changeState(event: MachineEvent): MachineStateWithTransition {
        switch (event) {
            case MachineEvent.ErrorCleared: return new InterruptedWithTransition(State.Error);
            case MachineEvent.ProgramCanceled: return new IdleWithTransition(State.Error);
            default:
                throw new UnexpectedEventError();
        }
    }
}

Execution Result

The result looks like the below.

export class Context2 {
    constructor(private machineState: MachineStateWithTransition) {
        this.machineState = machineState;
    }
    public changeState(event: MachineEvent): void {
        try {
            this.machineState = this.machineState.changeState(event);
            const state = State[this.machineState.currentState];

            const transition = this.machineState.transition !== null ?
                Transition[this.machineState.transition] : null;

            console.log(`${state.padEnd(15, " ")}\t${transition}`);
        } catch (e) {
            console.log(`!!! Error !!! ${e}`);
        }
    }
}

const context = new Context2(new NotSelectedWithTransition());

context.changeState(MachineEvent.Selected);
context.changeState(MachineEvent.ProgramStarted);
context.changeState(MachineEvent.Stopped);
context.changeState(MachineEvent.Started);
context.changeState(MachineEvent.Interrupted);
context.changeState(MachineEvent.ProgramCanceled);
context.changeState(MachineEvent.SelectCleared);
context.changeState(MachineEvent.Selected);
context.changeState(MachineEvent.ProgramStarted);
context.changeState(MachineEvent.Error);
context.changeState(MachineEvent.ErrorCleared);
context.changeState(MachineEvent.Started);
context.changeState(MachineEvent.Finished);
context.changeState(MachineEvent.ProgramCompleted);

// Idle            NotSelected_Idle
// Running         Idle_Running
// Stopped         Running_Stopped
// Running         Stopped_Running
// Interrupted     Running_Interrupted
// Idle            Interrupted_Idle
// NotSelected     Idle_NotSelected
// Idle            NotSelected_Idle
// Running         Idle_Running
// Error           Running_Error
// Interrupted     Error_Interrupted
// Running         Interrupted_Running
// Finished        Running_Finished
// Idle            Finished_Idle

The transition was shown correctly.

Conclusion

I tried to apply State pattern to the state machine but doesn’t reduce the complexity very much in this case. If there are several methods that need to have conditional clauses to handle the process depending on the state, State pattern can reduce the complexity a lot. In this case, the code becomes much more readable.

However, when we look at one of the classes, there is no other state-related logic there and thus it’s more readable than writing everything in a single class.

Did you compare the two implementations above? Which way do you prefer in this case?

Comments

Copied title and URL