Introduction
A Finite State Machine (FSM) is the significant model for designing dynamic behavior, i.e., to receive events from a finite collection and keep track of the current state and the previous state, and provide a mechanism for changing states based on an event from a set of predetermined events. The implementation is the state-transition system based on the trendy cell phone activities developed in .NET.
Finite State Machine
No FSM is an isolated process. Every aspect of an FSM depends on its context, requirements, limitation of programming language and other factors. The most popular non-object oriented implementations of FSM are cascading if statements, nested switch statements, state transition tables and code using goto statements. Their popularity is due to fast execution, but at the cost of weaker reliability and maintainability. On the other hand, the pure Object-Oriented FSM designs require �Complete Reification�, that is, making every element of the state machine an object. Finite State Machine implementation comes from �the Gang of Four� (GoF) with their design pattern �The State Pattern�. This is an object-oriented approach with a tightly coupled structure of classes or class hierarchy for each state and event.
A Finite State Machine has at least two common characteristics:
- First, a distinct, finite number of states exist.
- Second, methods exist that facilitate movement between the states, i.e., transition.
The State Pattern
Allow an object to alter its behavior when its internal state changes. The object will appear to change its class. The solution is provided in the following way; each state is encapsulated in a separate class that implements the common State interface (IState
) and defining the behavior to handle all external events. The Context
class is responsible for maintaining the knowledge of the current state and other data. External entities communicate with the system via the Context
class that forwards requests to the current state object. There are the following consequences while using the State Pattern.
- State specific behavior is localized and partitioned between different states.
- State transition is performed exactly in the code.
- If an object has many states, many concrete classes need to be created, which may result in an excessively large class hierarchy.
- The
Context
class delegates all external events to corresponding methods in the interface of the State
class; it is responsible for providing the correct mapping. This results in a tight coupling between the Context
class and concrete events.
- The
Context
class keeps one instance of each state in a hash table so that state changes does not result in creation or deletion of objects.
- The
Context
class keeps many state objects, but only one is active (i.e., responds to events) at a given time.
States
- Idle State � system is by default in Idle state. Moreover, whenever we cancel the other states, system switches to �Idle� sate.
- Menu State � system is in Idle state. If we invoke Menu command, state changes to Menu with the operation �Menu List�, and if we invoke Call command, state changes to Menu with the operation �LastCall List�.
- Dialing State � system is in Menu state. If we invoke Call command, state changes to Dialing with the operation �Calling� and this Calling state is then triggered to �Talking� with operation �Call Connected� after some wait, provided in the meanwhile we did not �Cancel� the state.
- Ringing State � system is in Idle state. If we invoke Ringing command, state changes to Ringing with the operation �Call is Coming�. If we invoke Call command, state changes to Talking with the operation �Answering�, and if we invoke Cancel command, state changes to Idle after disconnecting the action.
Special Cases
- If we are in Idle state and we invoke the Cancel command, system gives message �Already in Idle state�.
- If we are in Menu state and we invoke the Menu command, system gives message �Already in Menu state�.
- If we are in Dialing/Talking state and we invoke the Call command, system gives message �Already in Dialing/Talking state�.
- If we are in Talking state and we invoke the Menu command, system gives message �Operation Ignored: Currently in Talking State�.
- If we are in Ringing state and we invoke the Menu command, system gives message �Operation Ignored: Currently in Ringing State�.
Class Diagram
IState interface
The IState
interface is how you implement the actual "intelligence". The idea is that an entity can only be in one state at any given time, so you can use this interface to dictate what happens when that state is entered, when to leave that state, and where to go.
interface IState
{
void menuButton(CellPhone cellPhone);
void callButton(CellPhone cellPhone);
void exitButton(CellPhone cellPhone);
}
CellPhone Class
This simply dictates that the entity must keep track of the current state, the previous state, and provide a mechanism for changing state. I added the property m_previousState
because it might make the state machine a bit more versatile if your code is able to know what state it was previously in, thus having the ability to change its behavior.
public class CellPhone
{
private Form1 m_form;
private IState m_previousState;
private IState m_currentState;
private Hashtable m_states = new Hashtable();
public CellPhone(Form1 f)
{
m_states.Add( eState.IDLE, new Idle());
m_states.Add( eState.MENU, new Menu());
m_states.Add( eState.RINGING, new Ringing());
m_states.Add( eState.DIALING, new Dialing());
m_states.Add( eState.TALKING, new Talking());
m_currentState = (IState) m_states[eState.IDLE];
m_previousState = null;
m_form = f;
disconnect();
}
public eState getCurrentState()
{
foreach(object key in m_states.Keys)
{
if((IState)m_states[key] == m_currentState)
return (eState)key;
}
return eState.UNKNOWN;
}
public void setCurrentState(eState s)
{
m_previousState = m_currentState;
m_currentState = (IState) m_states[s];
}
public void menuButton()
{
m_currentState.menuButton( this );
}
public void callButton()
{
m_currentState.callButton( this );
}
public void exitButton()
{
m_currentState.exitButton( this );
}
public void ringButton()
{
setCurrentState( eState.RINGING );
ringing();
}
public void call() { Show("Calling..."); }
public void showLastCall() { Show("LastCall List..."); }
public void showMenu() { Show("Menu List..."); }
public void talking() { Show("Call Connected..."); }
public void ringing() { Show("Call is coming..."); }
public void answer() { Show("Answering..."); }
public void disconnect() { Show("Disconnected/Idle..."); }
public void Show(string s)
{
ListViewItem item = new ListViewItem();
item.Text = s;
if(m_previousState != null)
item.SubItems.Add( m_previousState.ToString().Replace("StatePattern.", ""));
else
item.SubItems.Add( "Unknown" );
item.SubItems.Add(m_currentState.ToString().Replace("StatePattern.", ""));
m_form.MainListView.Items.Add( item );
m_form.LState.Text =
m_currentState.ToString().Replace("StatePattern.", "");
m_form.LOperation.Text = s;
}
public void ignore()
{
string str = String.Format(@"Ignored: Currently in {0} state",
m_currentState.ToString());
m_form.MainListView.Items.Add( str.Replace("StatePattern.", "") );
}
public void repeat()
{
string str = String.Format(@"Already in {0} state",
m_currentState.ToString());
m_form.MainListView.Items.Add( str.Replace("StatePattern.", "") );
}
}
Concrete Classes
The Concrete classes are going to be the concrete implementation of the IState
interface. Notice how each class implements the IState
interface to define the rules when states transition to another state. Alpha
class is a thread to automatically trigger the state from calling to talking state.
class Idle : IState
{
public void menuButton(CellPhone cellPhone)
{
cellPhone.setCurrentState( eState.MENU );
cellPhone.showMenu();
}
public void callButton(CellPhone cellPhone)
{
cellPhone.setCurrentState( eState.MENU );
cellPhone.showLastCall();
}
public void exitButton(CellPhone cellPhone)
{
cellPhone.repeat();
}
}
class Menu : IState
{
public void menuButton(CellPhone cellPhone)
{
cellPhone.repeat();
}
public void callButton(CellPhone cellPhone)
{
Alpha a = new Alpha( cellPhone );
Thread t = new Thread(new ThreadStart(a.threadProc));
t.Start();
}
public void exitButton(CellPhone cellPhone)
{
cellPhone.setCurrentState( eState.IDLE );
cellPhone.disconnect();
}
}
class Ringing : IState
{
public void menuButton(CellPhone cellPhone)
{
cellPhone.ignore();
}
public void callButton(CellPhone cellPhone)
{
cellPhone.setCurrentState( eState.TALKING );
cellPhone.answer();
}
public void exitButton(CellPhone cellPhone)
{
cellPhone.setCurrentState( eState.IDLE );
cellPhone.disconnect();
}
}
class Dialing : IState
{
public void menuButton(CellPhone cellPhone)
{
cellPhone.ignore();
}
public void callButton(CellPhone cellPhone)
{
cellPhone.repeat();
}
public void exitButton(CellPhone cellPhone)
{
cellPhone.setCurrentState( eState.IDLE );
cellPhone.disconnect();
}
}
class Talking : IState
{
public void menuButton(CellPhone cellPhone)
{
cellPhone.ignore();
}
public void callButton(CellPhone cellPhone)
{
cellPhone.repeat();
}
public void exitButton(CellPhone cellPhone)
{
cellPhone.setCurrentState( eState.IDLE );
cellPhone.disconnect();
}
}
Thanks
My deep thanks to Agha M. Ali for his suggestions on the article.