2

I have a bank account program. Each account can be in Initial State or Trusted Account State (at present). In future new states may be added. Interest is not paid if it is in initial state. But 9% interest is paid if it is in Trusted Account State.

The following code works. But there is a tight coupling in the ChangeState() method. The InitialAccountState needs to know about the existence of TrustedAccountedState.. And if we add a new state called VeteranAccountedState, the ChangeState() method needs to be re-written in InitialAccountState class.

What is the best .Net 4.0 way to reduce this coupling?

State Pattern

The state pattern allows a object's state to change at any given moment which actually alters its behavior.

Coupling is not a problem in the State Pattern when the order of change of the states is pre-defined. For example, a traffic light will always change from Green—Yellow—Red. In this scenario coupling is not a problem – Green is sure that it is always going to be Yellow state in the next step. Refer Analysis Of State Machine Pattern

ABSTRACT State

abstract class AccountState
    {
        // Properties
        public BankAccount Account { get; set; }
        public double Balance { get; set; }

        protected double interest;
        protected double lowerLimit;
        protected double upperLimit;

        public abstract void Deposit(double amount);
        public abstract void PayInterest();
    }

CONCRETE

  class InitialAccountState : AccountState
    {
        public InitialAccountState(AccountState state) :this(state.Balance, state.Account)
        {

        }

        public InitialAccountState(double balance, BankAccount account)
        {
            this.Balance = balance;
            this.Account = account;
            Initialize();
        }

        private void Initialize()
        {
            lowerLimit = 0.0;
            upperLimit = 1000.0;
        }

        public override void Deposit(double amount)
        {
            Balance += amount;
            ChangeState();
        }

        public override void PayInterest()
        {
            throw new Exception("No Interest Allowed");
        }

        private void ChangeState()
        {
            if (Balance > upperLimit)
            {
                Account.State = new TrustedAccountedState(this);
            }
        }
    }

    class TrustedAccountedState : AccountState
    {
        public TrustedAccountedState(AccountState state): this(state.Balance, state.Account)
        {
        }

        public TrustedAccountedState(double balance, BankAccount account)
        {
            this.Balance = balance;
            this.Account = account;
            Initialize();
        }

        private void Initialize()
        {
            interest = 0.05;
            lowerLimit = 1000.0;
            upperLimit = 10000000.0;
        }

        public override void Deposit(double amount)
        {
            Balance += amount;
            ChangeState();
        }

        public override void PayInterest()
        {
            Balance += interest * Balance;
            ChangeState();
        }

        private void ChangeState()
        {
            if (Balance < lowerLimit)
            {
                Account.State = new InitialAccountState(this);
            }
        }
    }

CONTEXT

  class BankAccount
    {
        // Properties
        public AccountState State { get; set; }

        public double Balance
        {
            get { return State.Balance; }
        }


        // Constructor
        public BankAccount(string owner)
        {
            this.State = new InitialAccountState(0.0, this);
        }

        public void Deposit(double amount)
        {
            State.Deposit(amount);

            Console.WriteLine("Deposited {0:C} --- ", amount);
            Console.WriteLine(" Balance = {0:C}", this.Balance);
            Console.WriteLine(" Status = {0}", this.State.GetType().Name);
            Console.WriteLine("");
        }

        public void PayInterest()
        {
            State.PayInterest();
            Console.WriteLine("INTEREST PAID --- ");
            Console.WriteLine(" Balance = {0:C}", this.Balance);
            Console.WriteLine(" Status = {0}\n", this.State.GetType().Name);
        }
    }

CLIENT

class Program
    {
        static void Main(string[] args)
        {
            BankAccount account = new BankAccount("Jim Johnson");

            account.Deposit(500.0);
            account.Deposit(300.0);
            account.Deposit(550.0);
            account.PayInterest();

            Console.ReadKey();
        }
}

REFERENCES

  1. Analysis Of State Machine Pattern
  2. Is the State Design pattern scalable ?
  3. State Design Pattern
  4. Implementing the Specification Pattern in .NET
  5. Is the Specification Pattern obsolete?
  6. Specification pattern in C#
  7. Specifications in C# 3.0
  8. LINQ Expression Trees and the Specification Pattern
jaco0646
  • 15,303
  • 7
  • 59
  • 83
LCJ
  • 22,196
  • 67
  • 260
  • 418

1 Answers1

4

I would refactor out the changing of states to another class like a service class whose responsibility it is to understand how to change states thereby inverting the dependency and removing the tight coupling between states. so I would change the abstract class so that the protected properties are accessible.

abstract class AccountState
{
    // Properties
    public BankAccount Account { get; set; }
    public double Balance { get; set; }

    internal double interest;
    internal double lowerLimit;
    internal double upperLimit;

    public abstract void Deposit(double amount);
    public abstract void PayInterest();
}

add a StateChanger class...

public class StateChanger(){
   public AccountState ChangeState(AccountState state){
       if((state is InitialAccountState) && (state.Balance > state.upperLimit)){
           return new TrustedAccountedState(state);
       }
       if((state is TrustedAccountedState) && (state.Balance < state.lowerLimit))
       {
           return new InitialAccountState(state);
       }
       return state;
   }
}

remove the dependencies from the AccountState classes

class InitialAccountState : AccountState
{
    public InitialAccountState(AccountState state) :this(state.Balance, state.Account)
    {

    }

    public InitialAccountState(double balance, BankAccount account)
    {
        this.Balance = balance;
        this.Account = account;
        Initialize();
    }

    private void Initialize()
    {
        lowerLimit = 0.0;
        upperLimit = 1000.0;
    }

    public override void Deposit(double amount)
    {
        Balance += amount;
    }

    public override void PayInterest()
    {
        throw new Exception("No Interest Allowed");
    }

}

class TrustedAccountedState : AccountState
{
    public TrustedAccountedState(AccountState state): this(state.Balance, state.Account)
    {
    }

    public TrustedAccountedState(double balance, BankAccount account)
    {
        this.Balance = balance;
        this.Account = account;
        Initialize();
    }

    private void Initialize()
    {
        interest = 0.05;
        lowerLimit = 1000.0;
        upperLimit = 10000000.0;
    }

    public override void Deposit(double amount)
    {
        Balance += amount;
    }

    public override void PayInterest()
    {
        Balance += interest * Balance;
    }

}

Then your BackAccount class acts like a controller and it looks like

    class BankAccount
    {

    // Properties
    public AccountState State { get; set; }
    private StateChanger stateChanger;

    public double Balance
    {
        get { return State.Balance; }
    }


    // Constructor
    public BankAccount(string owner,StateChanger stateChanger)
    {
        this.State = new InitialAccountState(0.0, this);
        this.stateChanger = stateChanger;
    }

    public void Deposit(double amount)
    {
        State.Deposit(amount);
        State = stateChanger.ChangeState(State);

        Console.WriteLine("Deposited {0:C} --- ", amount);
        Console.WriteLine(" Balance = {0:C}", this.Balance);
        Console.WriteLine(" Status = {0}", this.State.GetType().Name);
        Console.WriteLine("");
    }

    public void PayInterest()
    {
        State.PayInterest();
        State = stateChanger.ChangeState(State);        
        Console.WriteLine("INTEREST PAID --- ");
        Console.WriteLine(" Balance = {0:C}", this.Balance);
        Console.WriteLine(" Status = {0}\n", this.State.GetType().Name);
    }
}

and for the main

    class Program
    {
        static void Main(string[] args)
        {
            StateChanger stateChanger = new StateChanger();
            BankAccount account = new BankAccount("Jim Johnson",stateChanger);

            account.Deposit(500.0);
            account.Deposit(300.0);
            account.Deposit(550.0);
            account.PayInterest();

            Console.ReadKey();
        }
}

By separating out the concern of managing the state, the AccountState classes are decoupled from one another and now if you need to add more states there is just 1 class that needs to be updated. The AccountState classes would then not need to keep state instead they would just define behaviour ie how a Deposit or PayInterest should behave if your account where a given state. You can still use the specification pattern in the statechanger class.

Warrenn enslin
  • 1,036
  • 8
  • 11