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 ::
- Create interface class for state
- Create functions for all actions (transitions) in interface state class.
- Create class for all the states derive it from interface state class.
- 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
- Create the context class
- Pass the context class object to State class functions that helps to change the state.
- 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;
}