State pattern – object states and their transitions

In a certain credit card business system, bank accounts have three states, and there are different behaviors in different states:

1) Normal state (balance is greater than 0), users can deposit or withdraw money;

2) Overdraft status (the balance is less than 0 and greater than -2000), the user can deposit or withdraw money, but needs to pay interest on the outstanding balance.

3) Restricted status (balance less than -2000), users can only make deposits and need to pay interest on the arrears.

Figure Pseudocode to achieve the above requirements

The above code has the following problems:

1) When obtaining the status, there are many if branches. If you add a few more statuses, you need to add judgment conditions, and it does not comply with the opening and closing principle.

2) When performing deposit and withdrawal operations, there are conditions for judging the status, and the behavior is restricted by the status.

In order to better design objects with multiple states, you can use a design pattern called the state pattern.

1 status mode

The State Pattern allows an object to change its behavior when its internal state changes, making the object appear to modify its state class. It is an object behavior model.

Figure State Pattern UML

Context: Environment class, an object with multiple states. Since the states of environment classes are diverse and objects behave differently in different states, the states are separated to form separate state classes.

State: Abstract state class, used to define an interface to encapsulate behavior related to a specific state of the environment class. Declare methods corresponding to various states in the abstract state class, and implement these methods in its subclasses.

ConcreteState: Concrete state class is a subclass of the abstract state class. Each subclass implements behavior related to a state of the environment class.

public class UserAccount {

    private double balance;

    private CardState cardState;

    public UserAccount(double balance) {
        this.balance = balance;
        cardState = new NormalCardState(this);
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    public void deposit(double money) {
        System.out.println("Save money " + money);
        cardState.deposit(money);
        changeState();
        System.out.println("Credit card balance: " + balance + ", status is: " + cardState.getState());
        System.out.println("---------------------------------");
    }

    public void withdraw(double money) {
        if (balance - money < 0) {
            System.out.println("Borrow money " + money + ", the interest rate is 0.01");
        } else {
            System.out.println("Withdrawal " + money);
        }
        cardState.withdraw(money);
        changeState();
        System.out.println("Credit card balance: " + balance + ", status is: " + cardState.getState());
        System.out.println("---------------------------------");
    }

    public void changeState() {
        if (balance > 0) {
            if (!"Normal".equals(cardState.getState())) cardState = new NormalCardState(this);
        } else if (balance > -2000) {
            if (!"Overdraft".equals(cardState.getState())) cardState = new OverdraftCardState(this);
        } else {
            if (!"limited".equals(cardState.getState())) cardState = new LimitationCardState(this);
        }
    }

    public void setCardState(CardState cardState) {
        this.cardState = cardState;
    }
}

public abstract class CardState {

    protected final UserAccount userAccount;

    public CardState(UserAccount userAccount) {
        this.userAccount = userAccount;
    }

    public abstract void deposit(double money); // Deposit

    public abstract void withdraw(double money); // Withdraw money

    public abstract void payInterest(); // Pay interest

    public abstract String getState(); // Get status

}

public class BankService {

    public static void main(String[] args) {
        //Open an account
        UserAccount userAccount = new UserAccount(1000);
        userAccount.withdraw(500);
        userAccount.deposit(200);
        userAccount.withdraw(1000);
        userAccount.deposit(100);
        userAccount.withdraw(2000);
        userAccount.withdraw(500);
    }

}

//Withdraw 500.0
//Credit card balance: 500.0, status is: normal
//---------------------------------
//Deposit money 200.0
//Credit card balance: 700.0, status is: normal
//---------------------------------
//Borrow money 1000.0, the interest rate is 0.01
//Credit card balance: -300.0, status is: overdraft
//---------------------------------
//Deposit money 100.0
//Pay interest: -3.0
//Credit card balance: -203.0, status is: overdraft
//---------------------------------
//Borrow money 2000.0, the interest rate is 0.01
//Interest payment: -2.0300000000000002
//Credit card balance: -2205.03, status is: restricted
//---------------------------------
//Borrow money 500.0, the interest rate is 0.01
//This account has been restricted and cannot be withdrawn
//Credit card balance: -2205.03, status is: restricted
//---------------------------------


public class NormalCardState extends CardState{

    public NormalCardState(UserAccount userAccount) {
        super(userAccount);
    }

    @Override
    public void deposit(double money) {
        userAccount.setBalance(userAccount.getBalance() + money);
    }

    @Override
    public void withdraw(double money) {
        userAccount.setBalance(userAccount.getBalance() - money);
    }

    @Override
    public void payInterest() {

    }

    @Override
    public String getState() {
        return "normal";
    }

}

public class OverdraftCardState extends CardState{

    public OverdraftCardState(UserAccount userAccount) {
        super(userAccount);
    }

    @Override
    public void deposit(double money) {
        payInterest();
        userAccount.setBalance(userAccount.getBalance() + money);
    }

    @Override
    public void withdraw(double money) {
        payInterest();
        userAccount.setBalance(userAccount.getBalance() - money);
    }

    @Override
    public void payInterest() {
        System.out.println("Interest payment: " + userAccount.getBalance() * 0.01);
        userAccount.setBalance(userAccount.getBalance() * ( 1 + 0.01));
    }

    @Override
    public String getState() {
        return "overdraft";
    }
}

public class LimitationCardState extends CardState{

    public LimitationCardState(UserAccount userAccount) {
        super(userAccount);
    }

    @Override
    public void deposit(double money) {
        payInterest();
        userAccount.setBalance(userAccount.getBalance() + money);
    }

    @Override
    public void withdraw(double money) {
        System.out.println("This account has been restricted and cannot withdraw money");
    }

    @Override
    public void payInterest() {
        System.out.println("Interest payment: " + userAccount.getBalance() * 0.01);
        userAccount.setBalance(userAccount.getBalance() * ( 1 + 0.01));
    }

    @Override
    public String getState() {
        return "restricted";
    }
}

After using the state pattern, during the coding process, you don’t need to focus on the specific state of the relationship, and you only need to focus on implementing the business in the specific state.

1.1 State transition method

In the state pattern, there are two ways of state transition of environment classes:

1) Complete the conversion in the environment class. (The above code is in this form)

2) Complete the conversion in the specific state class.

Figure Comparison of two state transition methods

If a new status class is added, both methods need to be modified in their respective classes. None of them conform to the open-close principle.

1.2 Sharing status

In some cases, multiple environment class objects need to share a state, in which case the state object needs to be defined as a static member object.

Requirements: There are two switches in a room to control the light bulbs. Functions such as switches are fixed (turning on can only make the bulb light up, turning off can only make the light bulb go out.

public class LightSwitch {

    private final static LightState onState = new OnLightState(),offState = new OffLightState();

    private static LightState lightState = offState;

    private final String name;

    public LightSwitch(String name) {
        this.name = name;
    }

    public static void changeLightState(String type) {
        if ("on".equalsIgnoreCase(type)) {
            lightState = onState;
        } else {
            lightState = offState;
        }
    }

    public void off() {
        System.out.println(name + "Close operation");
        lightState.off(this);
    }

    public void on() {
        System.out.println(name + "Open operation");
        lightState.on(this);
    }

}

public abstract class LightState {

    public abstract void on(LightSwitch lightSwitch);

    public abstract void off(LightSwitch lightSwitch);
}

public class OnLightState extends LightState{

    @Override
    public void on(LightSwitch lightSwitch) {
        System.out.println("The light bulb is on");
        System.out.println("--------------");
    }

    @Override
    public void off(LightSwitch lightSwitch) {
        System.out.println("Close successfully");
        LightSwitch.changeLightState("off");
        System.out.println("--------------");
    }

}

public class OffLightState extends LightState{

    @Override
    public void on(LightSwitch lightSwitch) {
        System.out.println("Opened successfully");
        LightSwitch.changeLightState("on");
        System.out.println("--------------");
    }

    @Override
    public void off(LightSwitch lightSwitch) {
        System.out.println("Lamp is off");
        System.out.println("--------------");
    }

}

public class PeopleOpera {
    public static void main(String[] args) {
        LightSwitch lightSwitch1 = new LightSwitch("Switch 1");
        LightSwitch lightSwitch2 = new LightSwitch("Switch 2");

        lightSwitch1.on();
        lightSwitch2.off();
        lightSwitch1.off();
        lightSwitch1.on();
        lightSwitch2.on();
        lightSwitch2.off();
    }
}

//Switch 1 opens operation
//Open successfully
//-------------
//Switch 2 close operation
//Close successfully
//-------------
//Switch 1 close operation
//Lamp is off
//-------------
//Switch 1 opens operation
//Open successfully
//-------------
//Switch 2 opens operation
//The light bulb is on
//-------------
//Switch 2 close operation
//Close successfully
//--------------

2 Advantages and Disadvantages

advantage:

1) The environment class conversion state method encapsulates the state conversion rules and centrally manages the state conversion code.

2) Encapsulate all behaviors related to specific states in a class.

3) Multiple environment objects can share a state object, thereby reducing the number of objects in the system.

4) Convert the state mode in the specific state class, synthesize the state transition logic with the state object, and avoid using huge conditional statement blocks to interweave business methods and state transition code.

shortcoming:

1) Increased the number of classes and objects.

2) The implementation is relatively complex, which increases the difficulty of system design.

3) Support for the open-closed principle is not good.

3 applicable scenarios

1) The behavior of an object depends on its state, and states convert between each other.

2) The code contains a large number of conditional statements related to the state of the object.