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
IFreezabletoIAccountState: Since we’re handling more states beyond just Frozen and Active, renaming the interface toIAccountStategives 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, andClosewith specific behaviors for each state.Add Callbacks for Balance Updates: Using callbacks within
DepositandWithdrawallows each state to determine if and when balance updates should occur.Refactor the
AccountClass: TheAccountclass 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 addToBalanceandAction 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
onUnfreezecallback in case the account later becomes frozen and needs to unfreeze. -
Deposit and Withdraw: Execute balance update callbacks and stay in the
Activestate. -
Freeze and Close: Transition the account to
FrozenorClosed, 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
onUnfreezeto notify when the account unfreezes. -
Deposit and Withdraw: Execute
_onUnfreezecallback and update balance, transitioning back toActive. -
Freeze: Keeps the account in the
Frozenstate if it’s already frozen. -
Close: Transitions to the
Closedstate.
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
onUnfreezecallback 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
Closedstate.
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
Closedstate, 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
NotVerifiedstate. - 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.
Top comments (0)