State Design Pattern

Introduction

 
This blog will help you to erase almost all of your doubts about state management in OOPS and help you write more scalable code.
 
I will explain everything using a WPF app with UI. You can download the attached code for reference.
 
I am not following the MVVM design pattern in this project;  rather, I'm using code behind because our main focus here is to understand state design pattern.
 
In order to understand MVVM you are welcomed to visit my other blog MVVM Design Pattern
 
Note
I am explaining this with a WPF application, so there is a lot of code that I can't put in here, I request you to download the attached source code. (Following are the all screens (state-wise).)
 
New state
 
 
After successful submit.
 
 
 
If user presses cancel.
 
 
 
If the date expires.
 
 
 
When an application is in closed state. 
 
 
 
If ticket is booked successfully.
 
 
 
If the user cancels after pending state.
 
 
First of all, why are we even doing this? What is wrong with a naive approach?
  • The greater the number of states, the more interdependent their logic becomes
  • More time to manage state across the application
  • Code is no longer extensible as it has way too many dependencies and it needs a lot of refactoring every time a new state is added or altered 
  • Harder to debug each state which is tangled with other states. I mean who are we kidding, I am a developer and I know how frustrating debugging can be.
Here state design pattern comes to the rescue: It minimizes the complexity and tackles all of the problems faced above
 
So what does state design pattern do?
  • It encapsulates state-specific behaviour within a separate state object
  • A class delegates the execution of its state-specific behaviour to one state at a time instead of implementing state-specific behaviour itself.
Have a good look at the following conceptual diagram. Don't worry for now what this means, once we code it will be cleared up.
 

 
 
Explanation of the diagram,
  1. The context is a class which maintains an instance of a concrete state as its current state. 
  2. The abstract state is an abstract class that defines an interface encapsulating all state-specific behaviours. 
  3. A concrete state is a subclass of the abstract state that implements behaviours specific to a particular state of the context.
Looked at another way, we have the context, an abstract state, and any number of concrete states.

The concrete states derive from the abstract state implementing the interfaces defined in it.

The context maintains a reference to one of the concrete states as its current state via the abstract state base class. 
 
State design pattern was developed to overcome 2 main challenges,
  1. How can an object change its behaviour when its internal state changes.
  2. How can state-specific behaviours be defined so that states can be added without altering the behaviour of existing states?
I will code both the naive approach and then a state transition pattern approach.
 
 Let's take a real-life example.
 
 Assume you're visiting IRCTC's website. while booking a ticket your object might fall under one of the following states.
 
 
 
Now here is the problem,
 
When the state is new, the user can go into submit state or also a user might cancel a booking and go into close-state or a date might expire and the object can go into close-state.
 
Now, these are a few scenarios.
 
How can I tackle this problem with a naive approach?  By creating a new boolean variable for each state and changing dependent code.
That just creates too much interdependency.
 
Assume we have a class Booking, which after following a naive approach might look like this.
 
Note
The naive approach may look easy because it's ready-made code, but it is very difficult to manage, knowing all the interdependencies.
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Text;  
  5. using System.Threading;  
  6. using System.Threading.Tasks;  
  7. using State_Design_Pattern.UI;  
  8.   
  9. namespace State_Design_Pattern.Logic  
  10. {  
  11.     public class Booking  
  12.     {  
  13.         private MainWindow View { getset; }  
  14.         public string Attendee { getset; }  
  15.         public int TicketCount { getset; }  
  16.         public int BookingID { getset; }  
  17.   
  18.         private CancellationTokenSource cancelToken;  
  19.         /// <summary>  
  20.         /// boolean to maintain new state  
  21.         /// </summary>  
  22.         bool isNew;  
  23.   
  24.         /// <summary>  
  25.         /// boolean to maintain pending state  
  26.         /// </summary>  
  27.         bool isPending;  
  28.   
  29.         /// <summary>  
  30.         /// boolean to maintain booked state  
  31.         /// </summary>  
  32.         bool isBooked;  
  33.         public Booking(MainWindow view)  
  34.         {  
  35.             View = view;  
  36.             isNew = true;  
  37.             BookingID = new Random().Next();  
  38.             ShowState("New");  
  39.             view.ShowEntryPage();  
  40.         }  
  41.   
  42.         public void SubmitDetails(string attendee, int ticketCount)  
  43.         {  
  44.             if (isNew)  
  45.             {  
  46.                 isNew = false;  
  47.                 isPending = true;  
  48.                 Attendee = attendee;  
  49.                 TicketCount = ticketCount;  
  50.   
  51.                 //to cancel submited booking.  
  52.                 cancelToken = new CancellationTokenSource();  
  53.                 StaticFunctions.ProcessBooking(this, ProcessingComplete, cancelToken);  
  54.                 ShowState("Pending");  
  55.                 View.ShowStatusPage("Processing booking");  
  56.             }  
  57.         }  
  58.   
  59.         public void Cancel()  
  60.         {  
  61.             if (isNew)  
  62.             {  
  63.                 ShowState("Booking cancelled");  
  64.                 View.ShowStatusPage("Cancelled by user");  
  65.                 isNew = false;  
  66.             }  
  67.             else if (isPending)  
  68.             {  
  69.                 cancelToken.Cancel();  
  70.             }  
  71.             else if (isBooked == true)  
  72.             {  
  73.                 ShowState("Closed");  
  74.                 View.ShowStatusPage("Booking canceled: Expect a refund");  
  75.                 isBooked = false;  
  76.             }  
  77.             else  
  78.             {  
  79.                 View.ShowStatusPage("Booking cannot be cancelled");  
  80.             }  
  81.         }  
  82.   
  83.         public void DatePassed()  
  84.         {  
  85.             if (isNew)  
  86.             {  
  87.                 ShowState("Booking cancelled");  
  88.                 View.ShowStatusPage("Booking expired!");  
  89.                 isNew = false;  
  90.             }  
  91.             else if (isBooked == true)  
  92.             {  
  93.                 ShowState("Closed");  
  94.                 View.ShowStatusPage("We hope you enjoyed the event");  
  95.                 isBooked = false;  
  96.             }  
  97.         }  
  98.   
  99.         public void ProcessingComplete(Booking booking, ProcessingResult result)  
  100.         {  
  101.             isPending = false;  
  102.             switch (result)  
  103.             {  
  104.                 case ProcessingResult.Sucess:  
  105.                     ShowState("Booked");  
  106.                     View.ShowStatusPage("Enjoy the Event");  
  107.                     break;  
  108.                 case ProcessingResult.Fail:  
  109.                     isNew = true;  
  110.                     View.ShowProcessingError();  
  111.                     Attendee = string.Empty;  
  112.                     BookingID = new Random().Next();  
  113.                     ShowState("New");  
  114.                     View.ShowEntryPage();  
  115.                     break;  
  116.                 case ProcessingResult.Cancel:  
  117.                     ShowState("Closed");  
  118.                     View.ShowStatusPage("Processing Canceled");  
  119.                     break;  
  120.             }  
  121.         }  
  122.   
  123.         public void ShowState(string stateName)  
  124.         {  
  125.             View.grdDetails.Visibility = System.Windows.Visibility.Visible;  
  126.             View.lblCurrentState.Content = stateName;  
  127.             View.lblTicketCount.Content = TicketCount;  
  128.             View.lblAttendee.Content = Attendee;  
  129.             View.lblBookingID.Content = BookingID;  
  130.         }  
  131.     }  

Problem with the above code,
 
First problem: When the booking was in a new-state: cancel method updates the UI saying it's cancelled, when the booking was in pending-state: cancel method updates the UI saying it's pending.

So each time a new state is added: A new boolean field is added to track the different state. New code has to be written to maintain the balance - which increases the complexity whenever a new-state is added.
 
I had to make and manage 3 booleans and tangle them with each other which makes code complex. Also note this is just one class; it took more than enough in other respected classes. 
 
Rather than doing this, we are going to do the following.
 
 
BookingContext is a context for the state pattern,

BookingState is an abstract class for the state pattern.
 
Let's add BookngState and BookingContext class. 
  1. namespace State_Design_Pattern.Logic  
  2. {  
  3.     public abstract class BookingState  
  4.     {  
  5.         public abstract void EnterState(BookingContext booking);  
  6.         public abstract void Cancel(BookingContext booking);  
  7.         public abstract void DatePassed(BookingContext booking);  
  8.         public abstract void EnterDetails(BookingContext booking, string attendee, int ticketCount);  
  9.     }  

Pay close attention to summaries, it describes the purpose of an object, a method or a variable.
 
BookingContext takes care of everything
1. a new state to begin with.
2. a transition method to change the state of an object.
3. calling UI a thread 
4. handling respective business logic.
  1. using State_Design_Pattern.UI;  
  2.  
  3.     /// <summary>  
  4.     /// the BookingContext class is a fully implemented context for our state pattern implementation.   
  5.     /// </summary>  
  6.     public class BookingContext  
  7.     {  
  8.         public MainWindow View { getprivate set; }  
  9.         public string Attendee { getset; }  
  10.         public int TicketCount { getset; }  
  11.         public int BookingID { getset; }  
  12.   
  13.         /// <summary>  
  14.         /// Maintains a reference to a concrete instance of the BookingState in a field called currentState  
  15.         /// </summary>  
  16.         BookingState currentState;  
  17.   
  18.         /// <summary>  
  19.         /// Sets the initial state of the process in its constructor  
  20.         /// </summary>  
  21.         /// <param name="view"></param>  
  22.         public BookingContext(MainWindow view)  
  23.         {  
  24.             View = view;  
  25.             TransitionToState(new NewState());  
  26.         }  
  27.   
  28.         /// <summary>  
  29.         /// provides a means for setting currentState in its TransitionToState method  
  30.         /// </summary>  
  31.         /// <param name="state"></param>  
  32.         public void TransitionToState(BookingState state)  
  33.         {  
  34.             currentState = state;  
  35.             currentState.EnterState(this);  
  36.         }  
  37.         public void SubmitDetails(string attendee, int ticketCount)  
  38.         {  
  39.             currentState.EnterDetails(this, attendee, ticketCount);  
  40.               
  41.         }  
  42.   
  43.         public void Cancel()  
  44.         {  
  45.             currentState.Cancel(this);  
  46.         }  
  47.   
  48.         /// <summary>  
  49.         ///  currentState.DatePassed also passing in this as the argument.   
  50.         ///  Now when the DatePassed method is called, instead of implementing any of the booking's behaviors here in the context,  
  51.         ///  the call is being passed to a method in the currentState.   
  52.         ///  In other words, the BookingContext is delegating the responsibility for handling the DatePassed method and   
  53.         ///  any state-specific behaviors associated with it to an instance of a concrete state, its currentState.   
  54.         ///  And the same is true of both the SubmitDetails and Cancel methods.  
  55.         ///  Instead of executing any behavioral code like updating the UI or submitting user information here on the context,  
  56.         ///  we're calling methods in the currentState, delegating to it the responsibility for executing whatever code makes sense to that specific state.  
  57.         /// </summary>  
  58.         public void DatePassed()  
  59.         {  
  60.             currentState.DatePassed(this);  
  61.         }  
  62.   
  63.         public void ShowState(string stateName)  
  64.         {  
  65.             View.grdDetails.Visibility = System.Windows.Visibility.Visible;  
  66.             View.lblCurrentState.Content = stateName;  
  67.             View.lblTicketCount.Content = TicketCount;  
  68.             View.lblAttendee.Content = Attendee;  
  69.             View.lblBookingID.Content = BookingID;  
  70.         }  
  71.     }  

Now add the concerned states as a class, which will be inherited by our abstract BookingState class.
 
All states are concrete classes, defining their own responsibilities towards their behaviour plus other states behaviours.
 
Let's add our first concrete class which defines new-state and takes care of following state transition and its own implementation.
 
 
 
When in the new state,  a booking can be cancelled resulting in it being in a closed state, or when the date for the event can pass also resulting in the booking being in a closed state. 
  1. using System;  
  2.   
  3. namespace State_Design_Pattern.Logic  
  4. {  
  5.     class NewState : BookingState  
  6.     {  
  7.         public override void Cancel(BookingContext booking)  
  8.         {  
  9.             booking.TransitionToState(new ClosedState("Booking cancelled"));  
  10.         }  
  11.   
  12.         public override void DatePassed(BookingContext booking)  
  13.         {  
  14.             booking.TransitionToState(new ClosedState("Booking expired"));  
  15.         }  
  16.   
  17.         public override void EnterDetails(BookingContext booking, string attendee, int ticketCount)  
  18.         {  
  19.             booking.Attendee = attendee;  
  20.             booking.TicketCount = ticketCount;  
  21.             booking.View.ShowStatusPage("Booking processing");  
  22.             booking.TransitionToState(new PendingState());  
  23.         }  
  24.   
  25.         public override void EnterState(BookingContext booking)  
  26.         {  
  27.             booking.BookingID = new Random().Next();  
  28.             booking.ShowState("New");  
  29.             booking.View.ShowEntryPage();  
  30.         }  
  31.     }  

Pending state
 
 
 
The user can submit information resulting in the booking being in a pending state.
  1. using System;  
  2. using System.Threading;  
  3.   
  4. namespace State_Design_Pattern.Logic  
  5. {  
  6.     class PendingState :BookingState  
  7.     {  
  8.         CancellationTokenSource CancellationToken;  
  9.         public override void Cancel(BookingContext booking)  
  10.         {  
  11.             CancellationToken.Cancel();  
  12.         }  
  13.   
  14.         public override void DatePassed(BookingContext booking)  
  15.         {  
  16.   
  17.         }  
  18.   
  19.         public override void EnterDetails(BookingContext booking, string attendee, int ticketCount)  
  20.         {  
  21.   
  22.         }  
  23.   
  24.         public override void EnterState(BookingContext booking)  
  25.         {  
  26.             CancellationToken = new CancellationTokenSource();  
  27.             booking.ShowState("Pending");  
  28.             booking.View.ShowStatusPage("Processing booking");  
  29.   
  30.             StaticFunctions.ProcessBooking(booking, ProcessingComplete, CancellationToken);  
  31.         }  
  32.   
  33.         private void ProcessingComplete(BookingContext booking, ProcessingResult result)  
  34.         {  
  35.             switch (result)  
  36.             {  
  37.                 case ProcessingResult.Sucess:  
  38.                     booking.TransitionToState(new BookedState());  
  39.                     break;  
  40.                 case ProcessingResult.Fail:  
  41.                     booking.View.ShowProcessingError();  
  42.                     booking.TransitionToState(new NewState());  
  43.                     break;  
  44.                 case ProcessingResult.Cancel:  
  45.                     booking.TransitionToState(new ClosedState("Processing cancelled"));  
  46.                     break;  
  47.                 default:  
  48.                     break;  
  49.             }  
  50.         }     
  51.     }  

And booked state,
 
A pending booking that returns successfully will result in the booking being booked.
 
 
A booking in the booked state can be cancelled in which case it transitions to the closed state, and we tell the user to expect a refund.
A booking in the closed state neither submitted or cancelled nor will it respond to the date for the event passing.
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Text;  
  5. using System.Threading.Tasks;  
  6.   
  7. namespace State_Design_Pattern.Logic  
  8. {  
  9.     class BookedState : BookingState  
  10.     {  
  11.         public override void Cancel(BookingContext booking)  
  12.         {  
  13.             booking.TransitionToState(new ClosedState("Booking cancelled, expect a refund."));  
  14.         }  
  15.   
  16.         public override void DatePassed(BookingContext booking)  
  17.         {  
  18.             booking.TransitionToState(new ClosedState("We hope you enjoyed the event."));  
  19.         }  
  20.   
  21.         public override void EnterDetails(BookingContext booking, string attendee, int ticketCount)  
  22.         {  
  23.   
  24.         }  
  25.   
  26.         public override void EnterState(BookingContext booking)  
  27.         {  
  28.             booking.ShowState("Booked");  
  29.             booking.View.ShowStatusPage("Enjoy journey");  
  30.         }  
  31.     }  

Now we took care of entire picture,
 

 
Awesome! Everything works perfectly. And you managed the behaviour of each of these states without resorting to Boolean fields and the conditional complexity of the naive approach. By using the state design pattern, you have code that's more modular, is easier to read and maintain, is less difficult to debug, and since the logic for each state is maintained in its own class, much easier to extend. New states can be added or the behaviours of existing states changed without the need to refactor the completed code of other states.
 
But it wouldn't be fair to close this blog without at least mentioning some of the potential drawbacks.
  1. First of all, they take some time to set up.
    This might not truly be a disadvantage as you get that time back and then some down the road, but it is something to consider.

  2. There are more moving parts.
    You completed the naive implementation in a single class, whereas the state pattern implementation of the same behaviours required six.
This isn't a bad thing in itself, but it has the potential of making your code more resource-intensive and in extreme cases can negatively impact performance.

 All things considered, the state design pattern is a great addition to your developer's go-to tool.

 So if you have objects in your apps that have easily identifiable states and the code in your method is starting to look like spaghetti, the state design pattern may be just the tool you're looking for.

I sincerely hope you enjoyed this blog and that you're inspired to apply what you've learned to your own applications.

Thank you.
 
Connect with me,