Design pattern - FSM - Finite state machine pattern in C++

 

Github link https://github.com/krishnaKSA/design_patterns_cplusplus/tree/main/FSM_Pattern

FINITE STATE MACHINE PATTERN

Normally state based behaviors can be written using state transitions (using switch cases). or we can use current to next state map using state table(map or 2D array).

However if more state gets added at later stage, or any exit or entry functions to get executed on every state will get complicated, if we go with above approach.
Debugging will be difficult if too many cases, enter, and exit added. So better to prefer the FSM design pattern based on the complexity of the problem.
reference :http://www.ai-junkie.com/architecture/state_driven/tut_state1.html


Problem: 

The machine starts in a locked state (Locked). When a coin is detected (Coin), the machine changes to the unlocked state (UnLocked) and open the turnstyle gate for the person to pass. When the machine detects that a person has passed (Pass) it turns back to the locked state. If a person attempts to pass while the machine is locked, an alarm is generated. If a coin is inserted while the machine is unlocked, a Thankyou message is displayed. When the machine fails to open or close the gate, a failure event (Failed) is generated and the machine enters the broken state (Broken). When the repair person fixes the machine, the fixed event (Fixed) is generated and the machine returns to the locked state.

 



 

FSM Model


 



Programming points ::

  1. Create interface class for state
  2. Create functions for all actions (transitions) in interface state class.
  3. Create class for all the states derive it from interface state class.
  4. Override functions in inherited state class only if the action need to care in that state otherwise let the default function get executed from the interface state class
  5. Create the context class
  6. Pass the context class object to State class functions that helps to change the state.
  7. Make sure singleton object created for all the state classes. In case any status or state value is used in interface class ,then define those variables at static. So that all the state classes access to the same variable. 


Github link https://github.com/krishnaKSA/design_patterns_cplusplus/tree/main/FSM_Pattern


//Header file


#ifndef COINMACHINESTATE_H
#define COINMACHINESTATE_H

#include <iostream>

using namespace std;

namespace coinMachine
{
    //machine states
    enum eMachineState
    {
        STATE_LOCKED = 0,
        STATE_UNLOCKED,
        STATE_BROKEN
    };

    //actions
    enum eStateActions
    {
        EVENT_COIN_DETECTED = 0,
        EVENT_PASS,
        EVENT_FIXED,
        EVENT_FAILED
    };

    //enum machine working state
    enum eMachineWorkingStatus
    {
        STATUS_MACHINE_INVALID = 0,
        STATUS_MACHINE_WORKING,
        STATUS_MACHINE_REPAIR
    };

    //enum gate status
    enum eMachineGateStatus
    {
        STATUS_GATE_INVALID = 0,
        STATUS_GATE_OPENED,
        STATUS_GATE_CLOSED
    };


    //forward declarations
    class CLockedState;
    class CCoinMachine;

    //Interface class for coin machine state and actions
    class IMachineState
    {
        private:
        static eMachineWorkingStatus workingStatus;
        static eMachineGateStatus gateStatus;
        static bool isInitialised;

        public:
        void initVariables()
        {
            //Init variables only once
            if (!isInitialised)
            {
                workingStatus = STATUS_MACHINE_WORKING;
                gateStatus = STATUS_GATE_CLOSED;
                isInitialised = true;
            }
        }
        //state action fuctions which has no actions to take
        //When called from the inappropriate state objects
        virtual void machineFailed(CCoinMachine* machine)
        {
            cout << "IMachineState::machineFailed:: wrong state ::no action " << endl;
        }
        virtual void coinDetected(CCoinMachine* machine)
        {
            cout << "IMachineState::coinDetected:: wrong state :: no action " << endl;
        }
        virtual void personPassed(CCoinMachine* machine)
        {
            cout << "IMachineState::personPassed:: wrong state :: no action " << endl;
        }
        virtual void machineFixed(CCoinMachine* machine)
        {
            cout << "IMachineState::machineFixed:: wrong state :: no action " << endl;
        }
        virtual void enterState(CCoinMachine* machine)
        {
            cout << "IMachineState::enterState:: wrong state :: no action " << endl;
        }
        virtual void exitState(CCoinMachine* machine)
        {
            cout << "IMachineState::exitState:: no action " << endl;
        }

        //behavioural functions
        bool openGate();

        bool closeGate();

        inline void setMachineStatus(const eMachineWorkingStatus status)
        {
            if (status != workingStatus)
            {
                workingStatus = status;
            }
        }
        //machine working status
        inline bool isMachineInWorkingStatus() const
        {
            return (workingStatus == STATUS_MACHINE_WORKING) ? (true):(false);
        }

        //set gate status
        inline void setGateStatus(const eMachineGateStatus status)
        {
            if (status != gateStatus)
            {
                gateStatus = status;
            }
        }

        //get gate status
        inline eMachineGateStatus getGateStatus() const
        {
            return gateStatus;
        }

        //utility functions
        inline void playAlarm()
        {
            cout << "Locked state:: ALARM!!! Person trying to pass in Locked State" << endl;
        }
        inline void printErrorMsg()
        {
            cout << "MACHINE OUT OF ORDER!!!!!!!" << endl;
        }
        inline void printThankYou()
        {
            cout << "THANK YOU!!!!!!!" << endl;
        }
        virtual ~IMachineState() {}
    };

    //FSM context class
    class CCoinMachine
    {
        IMachineState *machineState; //pointer to state class

        public:
        CCoinMachine();
        ~CCoinMachine() {}
        void setState(IMachineState *newState); //changestate
        void placeCoin();
    };

    //Locked state class
    class CLockedState: public IMachineState
    {
        private:
        static CLockedState* lockedStateObj; //singleton object

        public:
        static IMachineState* getLockedStateObj();

        //override functions
        void coinDetected(CCoinMachine* machine) override; //when coin detected
        void machineFailed(CCoinMachine* machine) override; // when machine failed
        void enterState(CCoinMachine* machine) override; //when entering to the locked state
        void exitState(CCoinMachine* machine) override; //when exiting from locked state
        void personPassed(CCoinMachine* machine) override; //when person passed

        //destructor
        ~CLockedState(){}        
    };


    //Unlocked state class
    class CUnlockedState: public IMachineState
    {
        private:
        static CUnlockedState* unlockedStateObj;//singleton object

        public:
        static IMachineState* getUnlockedStateObj();
   
        //override functions
        void coinDetected(CCoinMachine* machine) override; //when coin detected
        void machineFailed(CCoinMachine* machine) override; // when machine failed        
        void enterState(CCoinMachine* machine) override;  //when entering to the unlocked state  
        void exitState(CCoinMachine* machine) override;   //when exiting from unlocked state
        void personPassed(CCoinMachine* machine) override; //when person passed
        //destructor
        ~CUnlockedState(){}
    };    

    ////Broken state class
    class CBrokenState: public IMachineState
    {
        private:
        static CBrokenState* brokenStateObj;//singleton object

        public:
        static IMachineState* getBrokenStateObj();

        //override functions
        void enterState(CCoinMachine* machine) override; //when entering to the broken state  
        void exitState(CCoinMachine* machine) override {} //when exiting from broken state
        void machineFixed(CCoinMachine* machine) override; //when machine fixed

        //destructor
        ~CBrokenState() {}
    };
}

#endif

//Source file
#include <iostream>
#include "CoinMachine.h"

using namespace coinMachine;

/********* static variable initialization ***********/
CLockedState* CLockedState::lockedStateObj = 0;
CUnlockedState* CUnlockedState::unlockedStateObj = 0;
CBrokenState* CBrokenState::brokenStateObj = 0;
bool IMachineState::isInitialised = false;
eMachineWorkingStatus IMachineState::workingStatus = STATUS_MACHINE_INVALID;
eMachineGateStatus IMachineState::gateStatus = STATUS_GATE_INVALID;

/****** COIN MACHINE *****/

//constructor of CCoinMachine
CCoinMachine::CCoinMachine()
{
    machineState = CLockedState::getLockedStateObj(); //default state
    machineState->initVariables();
}

//Chane or set the new state
void CCoinMachine::setState(IMachineState* newState)
{
    machineState->exitState(this);//exit functionalities called for old state
    machineState = newState;
    machineState->enterState(this); //enter functionlities called for new state
}

//when user placed the coin
void CCoinMachine::placeCoin()
{
    machineState->coinDetected(this);
}

/***** INTERFACE STATE CLASS **********/

//open the gate
bool IMachineState::openGate()
{
    bool retStaus = false;
    if (isMachineInWorkingStatus())
    {
        setGateStatus(STATUS_GATE_OPENED);
        retStaus = true;
    }

    return retStaus;
}

//close the gate
bool IMachineState::closeGate()
{
    bool retStaus = false;
    if (isMachineInWorkingStatus())
    {
        setGateStatus(STATUS_GATE_CLOSED);
        return retStaus;
    }

    return retStaus;
}


/***** LOCKED STATE CLASS **********/

//get singleton object
IMachineState* CLockedState::getLockedStateObj()
{
    if (0 == lockedStateObj)
    {
        lockedStateObj = new CLockedState;
    }
    return lockedStateObj;
}

//COin detected in locked state
void CLockedState::coinDetected(CCoinMachine* machine)
{
    cout << "CLockedState::coin detected " << endl;
    machine->setState(CUnlockedState::getUnlockedStateObj());
   
}

//functionality when entering locked state
void CLockedState::enterState(CCoinMachine* machine)
{
    if (!isMachineInWorkingStatus())
    {
        machineFailed(machine); //if the machine failed
    }
    else
    {
        setGateStatus(STATUS_GATE_CLOSED); //close the gate
    }
}

//functionality when exit from locked state
void CLockedState::exitState(CCoinMachine* machine)
{
    //no avtion
}

//person passing whilst in locked state
void CLockedState::personPassed(CCoinMachine* machine)
{
    playAlarm();
}

//If machine in failed state
void CLockedState::machineFailed(CCoinMachine* machine)
{
    setMachineStatus(STATUS_MACHINE_REPAIR);
    machine->setState(CBrokenState::getBrokenStateObj());
}

/***** UNLOCKED STATE CLASS **********/

//COin detected in unlocked state
void CUnlockedState::coinDetected(CCoinMachine* machine)
{
    printThankYou();// when coin detected in unlocked state, print thank you
}

//If machine in failed state
void CUnlockedState::machineFailed(CCoinMachine* machine)
{
    setMachineStatus(STATUS_MACHINE_REPAIR);
    machine->setState(CBrokenState::getBrokenStateObj());
}

//person passing whilst in unlocked state
void CUnlockedState::personPassed(CCoinMachine* machine)
{
    cout << "CUnlockedState:: person passed" << endl;
    machine->setState(CLockedState::getLockedStateObj());
}

//functionality when entering unlocked state
void CUnlockedState::enterState(CCoinMachine* machine)
{
    cout << "CUnlockedState:: Entered to unlock state" << endl;
    if (openGate()) //if the gate opens, allow the person to pass
    {
        personPassed(machine);
    }
    else
    {   //if gate doesn't open
        machineFailed(machine);
    }

}

//functionality when exiting from unlocked state
void CUnlockedState::exitState(CCoinMachine* machine)
{
    if (isMachineInWorkingStatus())
    {
        //if the machine in working state , close the gate and print than you
        closeGate();
        printThankYou();
    }
}

//get instance of unlocked state object
IMachineState* CUnlockedState::getUnlockedStateObj()
{
    if (0 == unlockedStateObj)
    {
        unlockedStateObj = new CUnlockedState;
    }
    return unlockedStateObj;
}


/***** BROKEN STATE CLASS **********/

//get instance of broken state object
IMachineState* CBrokenState::getBrokenStateObj()
{
    if (0 == brokenStateObj)
    {
        brokenStateObj = new CBrokenState;
    }
    return brokenStateObj;
}


//If machine fixed
void CBrokenState::machineFixed(CCoinMachine* machine)
{
    setMachineStatus(STATUS_MACHINE_WORKING);
    machine->setState(CLockedState::getLockedStateObj());
}

//when machine goes to broken state
void CBrokenState::enterState(CCoinMachine* machine)
{
    printErrorMsg();
    machineFixed(machine); //Testing purpose added here
}

//Unit testing
#include <iostream>
#include "CoinMachine.h"

using namespace std;
using namespace coinMachine;

int main()
{
    //TESTING THE FUNCTIONALITY
    CCoinMachine *coinMachine = new CCoinMachine;
    uint8_t count = 0;
    while (count <= 25)
    {
        coinMachine->placeCoin();
        count++;
    }
    return 0;

}