State Design Pattern

This post is the continuation of Design Patterns Series and is related to the State Design Pattern. The State Design Pattern is used when the behaviour of an object changes based on the internal state of the object. Depending on the internal state of the object, the class or the type of the object is changed at the runtime. Lets look at an example to understand this better.

Problem Statement

Assume that we are building a banking software for managing loans of the customers. Loans can be of different types like housing loan, car loan, study loan, personal loan etc. For simplicity we’ll consider only one type of loan here in this example. Lets say that loan type is Personal Loan. The loan goes through different stages during its life cycle. In the beginning the process involves application, then approval, then repayment and finally the closure. We are not considering alternate scenarios like defaults etc. Depending on the current state of the loan, there can be different actions that can be performed. For example when the loan is being applied the applicant can’t pay the monthly instalment. Similarly when the loan is in repayment state and full amount has not been paid, it cannot be closed. Considering these business requirements, lets see how we can build the system.

Loan Manager without State Pattern

We start off with a simple Loan class which has a minimal functionality. We store the current state of the loan along with the approved amount and the outstanding amount.

        public Loan()

        {

            if (CurrentState == null)

            {

                CurrentState = LoanState.Applied;

            }

        }

 

        public LoanState CurrentState { get; private set; }

 

        public int ApprovedAmount { get; set; }

 

        public int OutstandingAmount { get; set; }

Initially, if the current state is not specified, we set the default value as applied. Now there are some business rules. You can make payments only if the loan is in the Repayment state. If the loan is currently in the Approved state, then the Authorized amount and the Outstanding amount are initialized. Imagine this is similar to bank approving your loan application and giving you the money. But payment is not possible in the Applied state and the Closed state. If the loan is not approved yet or it has already been closed as a result of payment of all the dues then there is no point of making any further payment. Based on these combination of rules here is our MakePayment method implementation

        public void MakePayment(int amount)

        {

            switch (CurrentState)

            {

                case LoanState.Reypayment:

                    Console.WriteLine("Updating outstanding amount...");

                    OutstandingAmount -= amount;

                    break;

                case LoanState.Approved:

                    Console.WriteLine("Updating approved and outstanding amount...");

                    ApprovedAmount = amount;

                    OutstandingAmount = amount;

                    break;

                case LoanState.Applied:

                case LoanState.Closed:

                    throw new Exception("Cannot make a payment in Applied or Closed state.");

            }

        }

Similarly, we need to update the state of the loan based on its current state. If the loan is in Applied state then we need to transition it to Approved state. From the Approved state, loan should be transitioned to Repayment state. While the Repayment is processed, if the outstanding amount is zero then we should change the state of the loan to Closed state. Based on these rules lets look at our implementation for the Process method.

        public void Process()

        {

            switch (CurrentState)

            {

                case LoanState.Applied:

                    Console.WriteLine("Updating state to approved from applied...");

                    CurrentState = LoanState.Approved;

                    break;

                case LoanState.Approved:

                    Console.WriteLine("Updating state to repayment from approved...");

                    CurrentState = LoanState.Reypayment;

                    break;

                case LoanState.Reypayment:

                    if (OutstandingAmount == 0)

                    {

                        Console.WriteLine("Updating state to closed from repayment...");

                        CurrentState = LoanState.Closed;

                    }

 

                    break;

            }

        }

Based on this implementation, lets look at a small LoanManager class which uses the Loan class to simulate a loan process.

        public LoanManager()

        {

            // Applied

            Loan loan = new Loan();

 

            loan.Process();

 

            // Approved

            loan.MakePayment(100);

            loan.Process();

 

            // Repayment

            loan.MakePayment(50);

            loan.Process();

 

            // Repayment

            loan.MakePayment(50);

            loan.Process();

 

            // Simulates exception in case of closed loan

            // loan.MakePayment(50);

            // loan.Process();

        }

The comments are explaining the flow of events how the loan transitions from one state to another when we call the Process method.

Problems with current implementation

There are few problems with the current implementation. The two methods MakePayment and Process are dependent on the current state of the loan. There is duplication of the switch statement within both the methods. Imagine what will happen if the business requirement changes and there are other states introduced in addition to the current set of states. For example, if the person who has taken loan does not repay the instalment in stipulated time the loan could go into a Default state.

If we follow the current approach every time there is modification to the list of states either addition or deletion, we will have to modify the two methods Process and MakePayment in our loan class. This is a very simplistic example. But in real applications there could be much more number of methods dependent on the state of the loan. If not corrected in time this can lead to lot of maintenance problems.

Refactoring towards State Pattern

If we observe carefully, the behaviour of the two methods is mainly dependent on the internal value of the current state of the loan. The behaviour changes when the state of the loan changes. In many places in the Gang Of Four book you’ll find advise to abstract what varies and encapsulate it into its own class. We can use this class in our code to comply with the Open Close Principle (OCP). By abstracting the functionality which varies we make sure that our class does not need to change every time there is a change in the varying class. Lets see how we can do this.

We define an abstract class with two methods which vary based on the state.

    public abstract class State

    {

        protected Loan _loan;

 

        public virtual void MakePayment(int amount)

        {

            throw new Exception("Cannot make a payment in current state.");

        }

 

        public virtual void Process()

        {

        }

    }

Each of the states derive from this class and implement the two methods as per their needs.

    public class AppliedState : State

    {

        public AppliedState(Loan loan)

        {

            _loan = loan;

        }

 

        public override void Process()

        {

            Console.WriteLine("updating current state from Applied to Approved...");

 

            _loan.CurrentState = new ApprovedState(_loan);

        }

    }

Here we see the implementation of AppliedState. Notice that the constructor takes the loan instance as an argument and stores it into an instance field. We override the Process method to set the current state to ApprovedState. Lets look at ApprovedState implementation

    public class ApprovedState : State

    {

        public ApprovedState(Loan loan)

        {

            _loan = loan;

        }

 

        public override void MakePayment(int amount)

        {

            _loan.ApprovedAmount = amount;

            _loan.OutstandingAmount = amount;

        }

 

        public override void Process()

        {

            Console.WriteLine("updating current state from Approved to Repayment...");

 

            _loan.CurrentState = new RepaymentState(_loan);

        }

    }

In this case we override both the MakePayment as well as Process methods with respect to the Approved state. With these changes in place lets look at the Loan class.

public class Loan

    {

        public Loan()

        {

            if (CurrentState == null)

            {

                CurrentState = new AppliedState(this);

            }

        }

 

        public int ApprovedAmount { get; set; }

 

        public int OutstandingAmount { get; set; }

 

        public State CurrentState { get; set; }

 

        public void MakePayment(int amount)

        {

            Console.WriteLine("Calling MakePayment on current state");

            CurrentState.MakePayment(amount);

        }

 

        public void Process()

        {

            Console.WriteLine("Calling Process on current state");

            CurrentState.Process();

        }

    }

Instead of storing an Enum value for the current state, now the Loan class stores an instance of the State type. Note the changes to the MakePayment and the Process method. These methods no longer rely on conditional code or switch case logic. Instead they just delegate to the current state instance to perform the appropriate action.

So how does the current state gets updated when there needs to be a transition from AppliedState to ApprovedState or ApprovedState to RepaymentState? This is possible because we pass the loan instance to the concrete state class. If you go back to the Process implementation in the AppliedState or the ApprovedState you’ll find the CurrentState of the passed in loan object being updated.

Conclusion

Using State Pattern we can make the code more maintainable. It is much easier to add or modify the logic if it is related to the state of the object. As we saw in the beginning of the post, if there is state based logic in a class, it can cause lot of duplication in the code. Apart from maintenance, state pattern is helpful in understanding the code flow. If you have a big chunk of switch case logic or if else construct which can transition into various different states, it can be problematic to understand. With State pattern it could be helpful to understand which states can be transitioned into other states very easily. Only disadvantage of State Pattern is that it increases the number of classes in your application.

By using individual states we can focus on the particulars of that state and its transitions. This also helps us in using the Single Responsibility Principle (SRP).

As always the complete working solution is available for download StateDesignPatternDemo.zip.

Until next time Happy  Programming.

Further Reading

Here are some books I recommend related to the topics discussed in this blog.

Share:
spacer

3 comments: