DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Flexible C# with OOP Principles:Assessing Improvement in Account State Management with the State Design Pattern in C#

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.

Image description


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:

  1. Rename IFreezable to IAccountState: Since we’re managing multiple states beyond just "Frozen" and "Active," renaming the interface to IAccountState gives it a more appropriate purpose.

  2. 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, and Close.

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

  4. Refactor the Account Class: The Account 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

  1. Define the IAccountState Interface: This interface defines the actions an account can perform, like Deposit, Withdraw, Freeze, HolderVerified, and Close. Each method returns an IAccountState object, enabling seamless transitions between states.
   public interface IAccountState
   {
       IAccountState Deposit(Action addToBalance);
       IAccountState Withdraw(Action subtractFromBalance);
       IAccountState Freeze();
       IAccountState HolderVerified();
       IAccountState Close();
   }
Enter fullscreen mode Exit fullscreen mode
  1. 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 or Closed.
  • 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.
  1. Refactor Account: The Account class now contains a reference to an IAccountState instance, which manages state-specific behavior. The Account class simply delegates actions like Deposit and Withdraw 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();
       }
   }
Enter fullscreen mode Exit fullscreen mode

Improved Testability and Object-Oriented Design

By refactoring with the State Design Pattern, we improved testability and modularity in several ways:

  1. 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 for Account, focusing on:

    • Ensuring the correct state interactions in Deposit and Withdraw.
    • Verifying balance updates through callbacks.
  2. 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.

  3. Scalability and Maintenance: Adding new states or modifying existing ones only requires changes within the state classes or the IAccountState interface. The Account 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 State Management Class Diagram

  • 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)