Mastering Complexity in OOP: How State-Based Logic Can Transform Your Code from Mess to Masterpiece
Simplifying Code with State Pattern in C#
You’re not alone if you’ve ever felt tangled in if-else or switch statements when dealing with objects in multiple states. These conditions can turn code into a mess—especially when managing different object behaviors as they move through various stages. Enter the State Pattern: a way to cleanly manage state-based behavior in a structured, maintainable way.
Today, we’ll walk through this idea step-by-step, using a straightforward Order Processing system as an example. We’ll explore how to handle state transitions without piling on if-else
checks and how the State Pattern can help us keep our code clean and scalable.
📌Explore more at: https://dotnet-fullstack-dev.blogspot.com/
🌟 Clapping would be appreciated! 🚀
The Problem with Traditional State Handling: A Mess of Conditionals
Imagine a simple order processing system where each order goes through the following stages:
- Pending
- Paid
- Shipped
- Delivered
- Cancelled
When managing each state, we may want to:
- Only ship orders that are paid.
- Only deliver orders that are shipped.
- Prevent certain actions if an order has been delivered or cancelled.
Most people handle this by adding a bunch of if-else
checks. Here’s how it might look in a traditional setup:
public class Order
{
public string Status { get; set; } = "Pending";
public void Pay()
{
if (Status == "Pending")
{
Status = "Paid";
Console.WriteLine("Order has been paid.");
}
else
{
Console.WriteLine("Cannot pay for order in current state: " + Status);
}
}
public void Ship()
{
if (Status == "Paid")
{
Status = "Shipped";
Console.WriteLine("Order has been shipped.");
}
else
{
Console.WriteLine("Cannot ship order in current state: " + Status);
}
}
public void Deliver()
{
if (Status == "Shipped")
{
Status = "Delivered";
Console.WriteLine("Order has been delivered.");
}
else
{
Console.WriteLine("Cannot deliver order in current state: " + Status);
}
}
public void Cancel()
{
if (Status == "Pending" || Status == "Paid")
{
Status = "Cancelled";
Console.WriteLine("Order has been cancelled.");
}
else
{
Console.WriteLine("Cannot cancel order in current state: " + Status);
}
}
}
What’s Wrong with This Approach?
- Too Many Conditions: Every action has multiple checks. As states grow, these checks get longer and harder to maintain.
- Scattered Logic: Each action has to know every possible state. This mixes different logic in one place, making the code messy.
- Difficult to Modify: If we add more states or actions, we have to update every
if-else
block across the code.
Using the State Pattern: A Better Approach to Managing States
The State Pattern allows an object to change its behavior based on its state by organizing each state’s behavior into its own class. With this approach:
- Each state has a dedicated class to handle its actions.
- We can transition between states without checking conditions everywhere.
- We keep the code clean, readable, and easy to extend.
Implementing the State Pattern
Let’s break it down step-by-step.
Step 1: Create a State Interface
We’ll define an interface, IOrderState
, with methods for each action our Order
class needs (e.g., Pay
, Ship
, Deliver
, Cancel
).
public interface IOrderState
{
void Pay(Order order);
void Ship(Order order);
void Deliver(Order order);
void Cancel(Order order);
}
Step 2: Create Classes for Each State
Now, each state (like Pending
, Paid
, Shipped
, etc.) gets its own class that implements IOrderState
. Each class only handles the actions allowed in that state.
For example, here’s what the PendingState
might look like:
public class PendingState : IOrderState
{
public void Pay(Order order)
{
order.State = new PaidState();
Console.WriteLine("Order has been paid.");
}
public void Ship(Order order) => Console.WriteLine("Cannot ship a pending order.");
public void Deliver(Order order) => Console.WriteLine("Cannot deliver a pending order.");
public void Cancel(Order order)
{
order.State = new CancelledState();
Console.WriteLine("Order has been cancelled.");
}
}
And here’s what the PaidState
might look like:
public class PaidState : IOrderState
{
public void Pay(Order order) => Console.WriteLine("Order is already paid.");
public void Ship(Order order)
{
order.State = new ShippedState();
Console.WriteLine("Order has been shipped.");
}
public void Deliver(Order order) => Console.WriteLine("Cannot deliver a paid order.");
public void Cancel(Order order)
{
order.State = new CancelledState();
Console.WriteLine("Order has been cancelled.");
}
}
Each state only handles the actions that make sense for it, making each class small and easy to understand.
Step 3: Define the Order Class as the Context
The Order
class (our context) maintains the current state. Instead of if-else
checks, it delegates work to the current state’s class.
public class Order
{
public IOrderState State { get; set; } = new PendingState();
public void Pay() => State.Pay(this);
public void Ship() => State.Ship(this);
public void Deliver() => State.Deliver(this);
public void Cancel() => State.Cancel(this);
}
The Order
class now just passes requests to its state object, and the state object takes care of the rest.
Testing the State-Based Logic
Let’s see how this looks when we test it out.
var order = new Order();
order.Pay(); // Output: Order has been paid.
order.Ship(); // Output: Order has been shipped.
order.Deliver(); // Output: Order has been delivered.
order.Cancel(); // Output: Cannot cancel a delivered order.
No if-else
statements, no hard-to-follow conditions—just smooth, state-based transitions.
Advantages of the State Pattern
- Organized Code: Each state class only deals with logic for that specific state, so code is cleaner and more organized.
- Scalability: If you add more states, you can just add new classes without changing existing logic.
- Readability: Each class clearly represents what can and can’t be done in a specific state, making it easier to understand and debug.
When to Use State-Based Logic
The State Pattern is useful when:
- An object has several states, each with different behavior.
- Complex conditions exist for different actions depending on the state.
- Scalability is important — if you plan to add more states or transitions, this approach will save you time and headaches.
Wrapping Up
Using the State Pattern makes code easier to understand, maintain, and scale. Instead of handling a chaotic mess of conditions, we’ve created a system where each state has clear, defined responsibilities. This structure helps ensure that as our application grows, our code remains clean and easy to modify.
So, next time you’re caught up in a web of conditions, think about giving the State Pattern a try! It might be just the “wow” factor your code needs.