DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Flexible C# with OOP Principles: Simplifying Account State Management with the State Design Pattern and Callbacks

Meta Description:

Learn how to simplify account state management in C# using the State Design Pattern and callbacks. This guide covers creating modular states like Active, Frozen, Not Verified, and Closed, enabling cleaner code and easier maintenance by encapsulating behavior within dedicated state classes. Perfect for developers seeking a scalable approach to managing complex state transitions in their applications.

Introduction

Managing different states within a single class often leads to branching logic that makes the code difficult to maintain. In this article, we’ll simplify account state management using the State Design Pattern and callbacks in C#. This approach keeps each state’s behavior in a separate class, resulting in cleaner and more modular code.


Updated Plan

  1. Rename IFreezable to IAccountState: Since we’re handling more states beyond just Frozen and Active, renaming the interface to IAccountState gives it a clearer, broader purpose.

  2. Add New States: We’ll create two additional states, Closed and NotVerified, alongside Active and Frozen. Each state will define methods like Deposit, Withdraw, Freeze, HolderVerified, and Close with specific behaviors for each state.

  3. Add Callbacks for Balance Updates: Using callbacks within Deposit and Withdraw allows each state to determine if and when balance updates should occur.

  4. Refactor the Account Class: The Account class will delegate all operations to the current state object, updating the balance through callbacks as appropriate, with no branching logic to determine behavior based on state.


Step 1: Define the IAccountState Interface

This interface defines actions an account can perform, allowing each state to control behavior. Each method returns an IAccountState object, enabling transitions to other states if needed.

// Defines possible actions an account can take in any given state
public interface IAccountState
{
    // Deposit action with a callback to update balance
    IAccountState Deposit(Action addToBalance);

    // Withdraw action with a callback to update balance
    IAccountState Withdraw(Action subtractFromBalance);

    // Action to freeze the account
    IAccountState Freeze();

    // Action to verify the account holder
    IAccountState HolderVerified();

    // Action to close the account
    IAccountState Close();
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Deposit and Withdraw: Use callbacks (Action addToBalance and Action subtractFromBalance) so the state can decide if and when to update the balance.
  • Freeze and Close: Triggered to change the account’s state if necessary.
  • HolderVerified: Used to mark the account as verified, which may lead to a state change.

Step 2: Implement the State Classes

We’ll implement each state class to define how it responds to different actions.

1. Active State

The Active state allows deposits and withdrawals, updating the balance via callbacks. The Freeze and Close methods transition the account to the Frozen and Closed states, respectively.

public class Active : IAccountState
{
    private readonly Action _onUnfreeze;

    // Constructor accepts an action to handle unfreezing in the future
    public Active(Action onUnfreeze)
    {
        _onUnfreeze = onUnfreeze;
    }

    // Deposits money by invoking the callback to add to the balance
    public IAccountState Deposit(Action addToBalance)
    {
        addToBalance?.Invoke();
        Console.WriteLine("Deposit made in Active state.");
        return this;
    }

    // Withdraws money by invoking the callback to subtract from the balance
    public IAccountState Withdraw(Action subtractFromBalance)
    {
        subtractFromBalance?.Invoke();
        Console.WriteLine("Withdrawal made in Active state.");
        return this;
    }

    // Transitions to the Frozen state
    public IAccountState Freeze()
    {
        Console.WriteLine("Account is now Frozen.");
        return new Frozen(_onUnfreeze);
    }

    // Holder is already verified in this state, so no change
    public IAccountState HolderVerified() => this;

    // Transitions to the Closed state
    public IAccountState Close() => new Closed();
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Constructor: Receives an onUnfreeze callback in case the account later becomes frozen and needs to unfreeze.
  • Deposit and Withdraw: Execute balance update callbacks and stay in the Active state.
  • Freeze and Close: Transition the account to Frozen or Closed, respectively.

2. Frozen State

In Frozen, deposits and withdrawals unfreeze the account, transitioning it back to Active. Each action invokes _onUnfreeze to signal this transition.

public class Frozen : IAccountState
{
    private readonly Action _onUnfreeze;

    // Constructor takes an action to handle unfreezing
    public Frozen(Action onUnfreeze)
    {
        _onUnfreeze = onUnfreeze;
    }

    // Unfreezes and deposits by invoking the unfreeze action, then returns to Active
    public IAccountState Deposit(Action addToBalance)
    {
        Console.WriteLine("Account is unfreezing with a deposit.");
        _onUnfreeze.Invoke();
        addToBalance?.Invoke();
        return new Active(_onUnfreeze);
    }

    // Unfreezes and withdraws by invoking the unfreeze action, then returns to Active
    public IAccountState Withdraw(Action subtractFromBalance)
    {
        Console.WriteLine("Account is unfreezing with a withdrawal.");
        _onUnfreeze.Invoke();
        subtractFromBalance?.Invoke();
        return new Active(_onUnfreeze);
    }

    // Stays in the Frozen state since it's already frozen
    public IAccountState Freeze()
    {
        Console.WriteLine("Account is already frozen.");
        return this;
    }

    // No change, as the holder is already verified
    public IAccountState HolderVerified() => this;

    // Transitions to the Closed state
    public IAccountState Close() => new Closed();
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Constructor: Receives onUnfreeze to notify when the account unfreezes.
  • Deposit and Withdraw: Execute _onUnfreeze callback and update balance, transitioning back to Active.
  • Freeze: Keeps the account in the Frozen state if it’s already frozen.
  • Close: Transitions to the Closed state.

3. NotVerified State

The NotVerified state restricts withdrawals and freezing until the account is verified. Deposits are accepted, and verification transitions the account to Active.

public class NotVerified : IAccountState
{
    private readonly Action _onUnfreeze;

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

    // Deposits money but keeps account in NotVerified state
    public IAccountState Deposit(Action addToBalance)
    {
        addToBalance?.Invoke();
        Console.WriteLine("Deposit accepted in NotVerified state.");
        return this;
    }

    // Blocks withdrawals until verification
    public IAccountState Withdraw(Action subtractFromBalance)
    {
        Console.WriteLine("Cannot withdraw from a non-verified account.");
        return this;
    }

    // Freezing is not allowed in the NotVerified state
    public IAccountState Freeze()
    {
        Console.WriteLine("Cannot freeze a non-verified account.");
        return this;
    }

    // Verifies the holder, transitioning to Active
    public IAccountState HolderVerified()
    {
        Console.WriteLine("Account holder verified, switching to Active state.");
        return new Active(_onUnfreeze);
    }

    // Closes the account, transitioning to Closed
    public IAccountState Close() => new Closed();
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Constructor: Receives an onUnfreeze callback in case the account later transitions to Frozen.
  • Deposit: Updates balance but stays in NotVerified.
  • Withdraw and Freeze: Restricted until the account is verified.
  • HolderVerified: Transitions to Active.
  • Close: Moves to the Closed state.

4. Closed State

In the Closed state, all actions are restricted, keeping the account permanently closed.

public class Closed : IAccountState
{
    public IAccountState Deposit(Action addToBalance)
    {
        Console.WriteLine("Cannot deposit to a closed account.");
        return this;
    }

    public IAccountState Withdraw(Action subtractFromBalance)
    {
        Console.WriteLine("Cannot withdraw from a closed account.");
        return this;
    }

    public IAccountState Freeze()
    {
        Console.WriteLine("Account is closed and cannot be frozen.");
        return this;
    }

    public IAccountState HolderVerified()
    {
        Console.WriteLine("Cannot verify a closed account.");
        return this;
    }

    public IAccountState Close()
    {
        Console.WriteLine("Account is already closed.");
        return this;
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • All Methods: Return the current Closed state, as all operations are restricted.

Step 3: Refactor the Account Class

The Account class contains only the current state and balance. It delegates actions to the current state, updating the balance via callbacks.

public class Account
{
    private IAccountState _state;
    public decimal Balance { get; private set; }

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



    // Deposit method, updates balance if allowed by state
    public void Deposit(decimal amount)
    {
        _state = _state.Deposit(() => Balance += amount);
    }

    // Withdraw method, updates balance if allowed by state
    public void Withdraw(decimal amount)
    {
        _state = _state.Withdraw(() => Balance -= amount);
    }

    // Freezes account if allowed by current state
    public void Freeze()
    {
        _state = _state.Freeze();
    }

    // Verifies holder if allowed by current state
    public void HolderVerified()
    {
        _state = _state.HolderVerified();
    }

    // Closes account if allowed by current state
    public void Close()
    {
        _state = _state.Close();
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Constructor: Initializes the account to the NotVerified state.
  • Deposit and Withdraw: Update balance based on the state’s response.
  • Freeze, HolderVerified, and Close: Delegate these actions to the state object, transitioning if necessary.

Testing the Implementation

Here’s a test to illustrate how the account system handles actions in different states:

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

        account.Deposit(100);      // Deposit accepted in NotVerified state
        account.HolderVerified();  // Account holder verified, switching to Active state
        account.Withdraw(50);      // Withdrawal made in Active state
        account.Freeze();          // Account is now Frozen
        account.Deposit(30);       // Account is unfreezing with a deposit
        account.Close();           // Account is now Closed
        account.Withdraw(10);      // Cannot withdraw from a closed account
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Using the State Design Pattern and callbacks, we created a modular and flexible account management system. Each state encapsulates its unique behavior, making state transitions dynamic and the Account class focused. This approach simplifies the Account class, providing a clean, maintainable design for managing complex state-based behavior.

Top comments (0)