Introduction
Refactoring large, complex classes into smaller, modular pieces not only improves readability but also enhances maintainability and scalability. In this article, we'll examine how we refactored an Account
class with multiple states (e.g., Active, Frozen, Not Verified, Closed) using the State Design Pattern in C#. This approach enables cleaner, more modular code, making the Account
class easier to manage and test.
Let’s explore the steps taken, the benefits of this refactoring, and analyze the final result.
Original Implementation and Problems
In the original code, all functionality was packed into a single Account
class. This led to a monolithic structure where the class had to manage all possible states and transitions, resulting in complex branching logic (e.g., if
statements for state checks). Such code is hard to read, test, and extend.
The Refactoring Plan
To make the Account
class more modular and easier to maintain, we adopted the State Design Pattern. Here’s the plan we followed:
Rename
IFreezable
toIAccountState
: Since we’re managing multiple states beyond just "Frozen" and "Active," renaming the interface toIAccountState
gives it a more appropriate purpose.Add New States: We created two additional states, Closed and NotVerified, along with the Active and Frozen states. Each state implements the methods for account operations, like
Deposit
,Withdraw
,Freeze
,HolderVerified
, andClose
.Add Callbacks for Balance Updates: Using callbacks within
Deposit
andWithdraw
allows each state to control when and if balance updates occur.Refactor the
Account
Class: TheAccount
class no longer needs branching logic for state management. Instead, it delegates operations to the current state object, updating the balance through callbacks as needed.
Implementing the State Design Pattern
-
Define the
IAccountState
Interface: This interface defines the actions an account can perform, likeDeposit
,Withdraw
,Freeze
,HolderVerified
, andClose
. Each method returns anIAccountState
object, enabling seamless transitions between states.
public interface IAccountState
{
IAccountState Deposit(Action addToBalance);
IAccountState Withdraw(Action subtractFromBalance);
IAccountState Freeze();
IAccountState HolderVerified();
IAccountState Close();
}
- Create State Classes: We implemented four classes—Active, Frozen, NotVerified, and Closed—each with its own response to these actions.
-
Active: Allows deposits and withdrawals. Can transition to
Frozen
orClosed
. -
Frozen: Allows unfreezing by depositing or withdrawing. Transitions to
Active
upon unfreezing. -
NotVerified: Restricts some actions until verified. Transitions to
Active
once verified. - Closed: Prevents all operations, as the account is permanently closed.
-
Refactor
Account
: TheAccount
class now contains a reference to anIAccountState
instance, which manages state-specific behavior. TheAccount
class simply delegates actions likeDeposit
andWithdraw
to the current state object.
public class Account
{
private IAccountState _state;
public decimal Balance { get; private set; }
public Account(Action onUnfreeze)
{
_state = new NotVerified(onUnfreeze); // Start in NotVerified state
}
public void Deposit(decimal amount)
{
_state = _state.Deposit(() => Balance += amount);
}
public void Withdraw(decimal amount)
{
_state = _state.Withdraw(() => Balance -= amount);
}
public void Freeze()
{
_state = _state.Freeze();
}
public void HolderVerified()
{
_state = _state.HolderVerified();
}
public void Close()
{
_state = _state.Close();
}
}
Improved Testability and Object-Oriented Design
By refactoring with the State Design Pattern, we improved testability and modularity in several ways:
-
Reduced Test Complexity: In the original design, testing the
Account
class required 11 tests to cover all possible states and branches. Now, we only need six tests forAccount
, focusing on:- Ensuring the correct state interactions in
Deposit
andWithdraw
. - Verifying balance updates through callbacks.
- Ensuring the correct state interactions in
Tests for State Classes: Each state’s behavior is tested separately in its own class. This keeps
Account
tests simple while ensuring each state class behaves as expected.Scalability and Maintenance: Adding new states or modifying existing ones only requires changes within the state classes or the
IAccountState
interface. TheAccount
class remains unaffected, as it delegates everything to the state objects.
Final Class Diagram
Below is the final class diagram illustrating the refactored Account
class, the IAccountState
interface, and the four concrete state classes.
- Account: Contains a reference to the current state and manages the balance. Delegates actions to the current state.
- IAccountState: Interface defining the contract for all state classes.
-
Active, Frozen, NotVerified, Closed: Concrete classes implementing
IAccountState
, each defining unique behaviors for their respective states.
Conclusion
This refactoring using the State Design Pattern has made our Account
class more object-oriented, modular, and testable. Each state now has its own dedicated class, reducing the responsibility of Account
to only balance management and delegating actions to its current state. This approach not only makes the code cleaner and easier to understand but also future-proofs it by allowing seamless addition of new states without modifying the core Account
logic.
By encapsulating state-specific logic into separate classes, we’ve achieved a design that adheres to single responsibility, encapsulation, and polymorphism principles, making the code highly maintainable and extensible. This refactored approach provides a robust, object-oriented foundation for managing complex state-based behavior in C#.
Top comments (0)