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.
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,
- The context is a class which maintains an instance of a concrete state as its current state.
- The abstract state is an abstract class that defines an interface encapsulating all state-specific behaviours.
- 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,
- How can an object change its behaviour when its internal state changes.
- 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.
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.
- namespace State_Design_Pattern.Logic
- {
- public abstract class BookingState
- {
- public abstract void EnterState(BookingContext booking);
- public abstract void Cancel(BookingContext booking);
- public abstract void DatePassed(BookingContext booking);
- public abstract void EnterDetails(BookingContext booking, string attendee, int ticketCount);
- }
- }
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.
- using State_Design_Pattern.UI;
-
-
-
-
- public class BookingContext
- {
- public MainWindow View { get; private set; }
- public string Attendee { get; set; }
- public int TicketCount { get; set; }
- public int BookingID { get; set; }
-
-
-
-
- BookingState currentState;
-
-
-
-
-
- public BookingContext(MainWindow view)
- {
- View = view;
- TransitionToState(new NewState());
- }
-
-
-
-
-
- public void TransitionToState(BookingState state)
- {
- currentState = state;
- currentState.EnterState(this);
- }
- public void SubmitDetails(string attendee, int ticketCount)
- {
- currentState.EnterDetails(this, attendee, ticketCount);
-
- }
-
- public void Cancel()
- {
- currentState.Cancel(this);
- }
-
-
-
-
-
-
-
-
-
-
-
- public void DatePassed()
- {
- currentState.DatePassed(this);
- }
-
- public void ShowState(string stateName)
- {
- View.grdDetails.Visibility = System.Windows.Visibility.Visible;
- View.lblCurrentState.Content = stateName;
- View.lblTicketCount.Content = TicketCount;
- View.lblAttendee.Content = Attendee;
- View.lblBookingID.Content = BookingID;
- }
- }
- }
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.
- using System;
-
- namespace State_Design_Pattern.Logic
- {
- class NewState : BookingState
- {
- public override void Cancel(BookingContext booking)
- {
- booking.TransitionToState(new ClosedState("Booking cancelled"));
- }
-
- public override void DatePassed(BookingContext booking)
- {
- booking.TransitionToState(new ClosedState("Booking expired"));
- }
-
- public override void EnterDetails(BookingContext booking, string attendee, int ticketCount)
- {
- booking.Attendee = attendee;
- booking.TicketCount = ticketCount;
- booking.View.ShowStatusPage("Booking processing");
- booking.TransitionToState(new PendingState());
- }
-
- public override void EnterState(BookingContext booking)
- {
- booking.BookingID = new Random().Next();
- booking.ShowState("New");
- booking.View.ShowEntryPage();
- }
- }
- }
Pending state
The user can submit information resulting in the booking being in a pending state.
- using System;
- using System.Threading;
-
- namespace State_Design_Pattern.Logic
- {
- class PendingState :BookingState
- {
- CancellationTokenSource CancellationToken;
- public override void Cancel(BookingContext booking)
- {
- CancellationToken.Cancel();
- }
-
- public override void DatePassed(BookingContext booking)
- {
-
- }
-
- public override void EnterDetails(BookingContext booking, string attendee, int ticketCount)
- {
-
- }
-
- public override void EnterState(BookingContext booking)
- {
- CancellationToken = new CancellationTokenSource();
- booking.ShowState("Pending");
- booking.View.ShowStatusPage("Processing booking");
-
- StaticFunctions.ProcessBooking(booking, ProcessingComplete, CancellationToken);
- }
-
- private void ProcessingComplete(BookingContext booking, ProcessingResult result)
- {
- switch (result)
- {
- case ProcessingResult.Sucess:
- booking.TransitionToState(new BookedState());
- break;
- case ProcessingResult.Fail:
- booking.View.ShowProcessingError();
- booking.TransitionToState(new NewState());
- break;
- case ProcessingResult.Cancel:
- booking.TransitionToState(new ClosedState("Processing cancelled"));
- break;
- default:
- break;
- }
- }
- }
- }
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.
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Threading.Tasks;
-
- namespace State_Design_Pattern.Logic
- {
- class BookedState : BookingState
- {
- public override void Cancel(BookingContext booking)
- {
- booking.TransitionToState(new ClosedState("Booking cancelled, expect a refund."));
- }
-
- public override void DatePassed(BookingContext booking)
- {
- booking.TransitionToState(new ClosedState("We hope you enjoyed the event."));
- }
-
- public override void EnterDetails(BookingContext booking, string attendee, int ticketCount)
- {
-
- }
-
- public override void EnterState(BookingContext booking)
- {
- booking.ShowState("Booked");
- booking.View.ShowStatusPage("Enjoy journey");
- }
- }
- }
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.
- 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.
- 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,