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
Rename
IFreezable
toIAccountState
: Since we’re handling more states beyond just Frozen and Active, renaming the interface toIAccountState
gives it a clearer, broader purpose.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
, andClose
with specific behaviors for each state.Add Callbacks for Balance Updates: Using callbacks within
Deposit
andWithdraw
allows each state to determine if and when balance updates should occur.Refactor the
Account
Class: TheAccount
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();
}
Explanation:
-
Deposit and Withdraw: Use callbacks (
Action addToBalance
andAction 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();
}
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
orClosed
, 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();
}
Explanation:
-
Constructor: Receives
onUnfreeze
to notify when the account unfreezes. -
Deposit and Withdraw: Execute
_onUnfreeze
callback and update balance, transitioning back toActive
. -
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();
}
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;
}
}
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();
}
}
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
}
}
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.