DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Flexible C# with OOP Principles:Simplified State Management

Introduction

Imagine an account that can be either active or frozen. When it’s frozen, deposits or withdrawals should unfreeze it, while other operations should freeze it. Instead of handling these transitions directly within the Account class, we’ll separate each state’s behavior into its own class. This way, Account becomes simpler because it just delegates to the current state.

Step 1: Define the IFreezable Interface

The IFreezable interface will specify what each account state (active or frozen) should be able to do:

  • Deposit: React to a deposit (this might unfreeze the account).
  • Withdraw: React to a withdrawal.
  • Freeze: Change the account to a frozen state.
public interface IFreezable
{
    IFreezable Deposit();
    IFreezable Withdraw();
    IFreezable Freeze();
}
Enter fullscreen mode Exit fullscreen mode

Each method returns a new IFreezable object, which lets us easily switch to a new state if necessary.

Step 2: Create Active and Frozen State Classes

We’ll create two classes, Active and Frozen, each handling the state-specific behavior. This way, we keep the Account class clean and focused on managing its state without handling the state logic directly.

Active State

In the Active state, deposits and withdrawals don’t change the state. However, calling Freeze switches the account to the Frozen state.

public class Active : IFreezable
{
    private readonly Action _onUnfreeze;

    public Active(Action onUnfreeze)
    {
        _onUnfreeze = onUnfreeze;
    }

    public IFreezable Deposit()
    {
        // Stay in Active state
        return this;
    }

    public IFreezable Withdraw()
    {
        // Stay in Active state
        return this;
    }

    public IFreezable Freeze()
    {
        // Transition to Frozen state
        return new Frozen(_onUnfreeze);
    }
}
Enter fullscreen mode Exit fullscreen mode

Frozen State

In the Frozen state, a deposit or withdrawal unfreezes the account (returning to Active). We also trigger the _onUnfreeze action as a notification of this transition.

public class Frozen : IFreezable
{
    private readonly Action _onUnfreeze;

    public Frozen(Action onUnfreeze)
    {
        _onUnfreeze = onUnfreeze;
    }

    public IFreezable Deposit()
    {
        // Trigger the action to unfreeze, switch to Active
        _onUnfreeze.Invoke();
        return new Active(_onUnfreeze);
    }

    public IFreezable Withdraw()
    {
        // Trigger the action to unfreeze, switch to Active
        _onUnfreeze.Invoke();
        return new Active(_onUnfreeze);
    }

    public IFreezable Freeze()
    {
        // Already frozen, stay in Frozen state
        return this;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Refactor the Account Class

The Account class holds a reference to an IFreezable object representing the current state. Whenever a deposit, withdrawal, or freeze action happens, it delegates the action to the state object and updates its state based on the returned result.

public class Account
{
    private IFreezable _state;

    public Account(Action onUnfreeze)
    {
        _state = new Active(onUnfreeze);  // Start in Active state
    }

    public void Deposit()
    {
        // Delegate to current state and update to new state
        _state = _state.Deposit();
    }

    public void Withdraw()
    {
        // Delegate to current state and update to new state
        _state = _state.Withdraw();
    }

    public void Freeze()
    {
        // Delegate to current state and update to new state
        _state = _state.Freeze();
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Testing the Implementation

To test this design, we’ll create an instance of Account with a simple OnUnfreeze action that logs a message when the account unfreezes.

class Program
{
    static void Main(string[] args)
    {
        Action onUnfreeze = () => Console.WriteLine("Account has been unfrozen.");
        Account account = new Account(onUnfreeze);

        account.Deposit();   // Account stays active
        account.Freeze();    // Account becomes frozen
        account.Deposit();   // Account unfreezes and returns to active
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Benefits of Using the State Design Pattern

  1. Simplifies the Account Class: The account class is easier to read and maintain because it no longer manages state changes directly.

  2. Encapsulates State Logic: The logic for each state is fully contained within its own class (Active and Frozen), making the code easier to understand.

  3. Flexible and Scalable: This design makes it easy to add more states or modify existing ones without changing the Account class.

Summary

By using the State Design Pattern, we offload complex state-related logic to specific state classes, making the Account class cleaner and more focused. This approach allows each state to manage its behavior, ensuring that our design remains modular and scalable.

This pattern is particularly useful for systems that involve multiple states, as it lets each state handle its own actions and transitions, making the codebase easier to maintain and extend.

Top comments (0)