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();
}
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);
}
}
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;
}
}
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();
}
}
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
}
}
Key Benefits of Using the State Design Pattern
Simplifies the
Account
Class: The account class is easier to read and maintain because it no longer manages state changes directly.Encapsulates State Logic: The logic for each state is fully contained within its own class (
Active
andFrozen
), making the code easier to understand.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)