Skip to main content

Design Patterns: State Pattern

Free2015-03-07#Design_Pattern#状态模式#State Pattern

The State Pattern is used to encapsulate a complete set of behaviors under a certain state. The State Pattern hides the state transition process; the caller is not aware of the internal state transition details within the module. The State Pattern achieves runtime polymorphism. If your code contains a large number of similar if-else structures, you may need to use the State Pattern to eliminate these discordant conditional blocks.

no_mkd

I. What is the State Pattern?

Encapsulate all actions within state objects; the state holder delegates behaviors to the current state object

In other words, the state holder (such as a car, television, or ATM machine, all having multiple states) is not aware of the action details. The state holder only cares about its current state (which state object it holds) and leaves everything to the current state object to handle. It doesn't even need to control state transitions (of course, the state holder has the right to control state transitions, but can also choose to be a hands-off manager..)

II. An Example

Suppose we want to simulate an ATM machine with the following requirements:

  • Withdraw cash, verify card password, dispense cash, end service
  • If password verification fails or balance is insufficient, eject the card directly and end this service
  • If the machine has no cash, display No Cash and send a no-cash message to the bank
  • If the machine malfunctions, display No Service and send a repair request to maintenance personnel

Then the user withdrawal process should look like this:

This is the user operation flow. For the ATM machine, we also need to pay attention to the following points:

  • After receiving the user's input amount, not only verify the user's card balance, but also verify the ATM's balance
  • After each successful withdrawal service, check the ATM's balance (if no cash, perform corresponding processing)
  • If any unprocessable error occurs at any step, handle it as a malfunction

Thinking about the withdrawal process above, we must also consider various illegal operations, such as:

  • Entering password without inserting card
  • Continuing operation when the machine has malfunctioned
  • Requesting withdrawal when the machine has no cash

After breaking it down, we find that detailed issues become very troublesome. The machine sits there with all user interfaces open; users could potentially use any interface at any time. To prevent these illegal operations, we have to add a series of checks, such as:

  • Before verifying password, check whether the card has been inserted and whether the machine has malfunctioned
  • Before inserting card, check whether the machine has malfunctioned
  • Before withdrawal operation, check whether the machine has malfunctioned, whether it has no cash, whether...

We need to set up a large number of member variables in our code to identify the machine's state. More terrifyingly, our logic blocks contain大量 if-else statements, and the if-else blocks in each operation have only subtle differences. They look redundant, but are difficult to handle (even if we extract each judgment as an independent method, which can reduce code size, the processing remains redundant..)

A better approach is to use the State Pattern, which can completely eliminate redundancy in state judgment parts and provide a clear and tidy code structure. Below, we implement the requirements in the example using the State Pattern

(1) First, identify all interfaces provided by the ATM

  1. Insert card
  2. Submit password
  3. Withdraw cash (assuming the withdrawal button is a physical key)
  4. Query (assuming same as above)
  5. Eject card

(2) Then identify all ATM states and the corresponding actions for each state

  • Ready (Ready), available interfaces: all
  • No Cash (NoCash), available interfaces: 1, 2, 4, 5
  • No Service (NoService), available interfaces: none

(3) Coding implementation

First, define the State base class, which encapsulates all interfaces listed in (1):

package StatePattern;

/**

  • Define ATM machine states

  • @author ayqy / public interface ATMState { /*

    • Insert card */ public abstract void insertCard();

    /**

    • Submit password */ public abstract void submitPwd();

    /**

    • Withdraw cash */ public abstract void getCash();

    /**

    • Query balance */ public abstract void queryBalance();

    /**

    • Eject card */ public abstract void ejectCard(); }

Then implement the three states one by one

ReadyState:

package StatePattern;

/**

  • Implement ATM ready state

  • @author ayqy */ public class ReadyState implements ATMState{ private ATM atm;//Keep reference to state holder for operations on it

    public ReadyState(ATM atm){ this.atm = atm; }

    @Override public void insertCard() { System.out.println("Card insertion completed"); }

    @Override public void submitPwd() { System.out.println("Password submission completed"); //Verify password and perform corresponding processing if("123".equals(atm.getPwd())){ System.out.println("Password verification passed"); } else{ System.out.println("Password verification failed"); //Eject card ejectCard(); } }

    @Override public void getCash() { if(atm.getTotalAmount() >= atm.getAmount() && atm.getBalance() >= atm.getAmount()){ //Update account balance atm.setBalance(atm.getBalance() - atm.getAmount()); //Update total cash amount in machine atm.setTotalAmount(atm.getTotalAmount() - atm.getAmount()); System.out.println("Dispense ¥" + atm.getAmount()); System.out.println("Withdrawal completed"); //Eject card ejectCard(); //Check remaining cash in machine if(atm.getTotalAmount() == 0){//If no cash, switch to NoCash state atm.setCurrState(atm.getNoCashState()); System.out.println("No-cash information has been sent to bank"); } } else{ System.out.println("Withdrawal failed, insufficient balance"); //Eject card ejectCard(); } }

    @Override public void queryBalance() { System.out.println("Balance ¥" + atm.getBalance()); System.out.println("Balance query completed"); }

    @Override public void ejectCard() { System.out.println("Card ejection completed"); } }

Note the state transition part in our state class:

if(atm.getTotalAmount() == 0){//If no cash, switch to NoCash state
	atm.setCurrState(atm.getNoCashState());
}

We don't directly new concrete state objects; instead, we use the set interface provided by ATM. This is done to achieve loose coupling as much as possible (sibling objects don't know each other) and gain more flexibility

Implement NoCashState:

package StatePattern;

/**

  • Implement ATM no-cash state

  • @author ayqy */ public class NoCashState implements ATMState{ private ATM atm;//Keep reference to state holder for operations on it

    public NoCashState(ATM atm){ this.atm = atm; }

    @Override public void insertCard() { System.out.println("Card insertion completed"); }

    @Override public void submitPwd() { System.out.println("Password submission completed"); //Verify password and perform corresponding processing if("123".equals(atm.getPwd())){ System.out.println("Password verification passed"); } else{ System.out.println("Password verification failed"); //Eject card ejectCard(); } }

    @Override public void getCash() { System.out.println("Withdrawal failed, no cash in machine"); }

    @Override public void queryBalance() { System.out.println("Balance ¥" + atm.getBalance()); System.out.println("Balance query completed"); }

    @Override public void ejectCard() { System.out.println("Card ejection completed"); } }

Implement NoServiceState:

package StatePattern;

/**

  • Implement ATM malfunction state

  • @author ayqy */ public class NoServiceState implements ATMState{ private ATM atm;//Keep reference to state holder for operations on it

    public NoServiceState(ATM atm){ this.atm = atm; }

    @Override public void insertCard() { System.out.println("Card insertion failed, machine has malfunctioned"); }

    @Override public void submitPwd() { System.out.println("Password submission failed, machine has malfunctioned"); }

    @Override public void getCash() { System.out.println("Withdrawal failed, machine has malfunctioned"); }

    @Override public void queryBalance() { System.out.println("Balance query failed, machine has malfunctioned"); }

    @Override public void ejectCard() { System.out.println("Card ejection failed, machine has malfunctioned"); } }

With concrete states implemented, we can construct the ATM class, like this:

package StatePattern;

/**

  • Implement ATM machine

  • @author ayqy */ public class ATM { /All states/ private ATMState readyState; private ATMState noCashState; private ATMState noServiceState;

    private ATMState currState;//Current state private int totalAmount;//Total cash amount in machine

    /Temporary variables for testing/ private String pwd;//Password private int balance;//Balance private int amount;//Withdrawal amount

    public ATM(int totalAmount, int balance, int amount, String pwd) throws Exception{ //Initialize all states readyState = new ReadyState(this); noCashState = new NoCashState(this); noServiceState = new NoServiceState(this);

     if(totalAmount > 0){
     	currState = readyState;
     }
     else if(totalAmount == 0){
     	currState = noCashState;
     }
     else{
     	throw new Exception();
     }
     
     //Initialize test data
     this.totalAmount = totalAmount;
     this.balance = balance;
     this.amount = amount;
     this.pwd = pwd;
    

    }

    /Delegate concrete behaviors to state objects/ /**

    • Insert card */ public void insertCard(){ currState.insertCard(); }

    /**

    • Submit password */ public void submitPwd(){ currState.submitPwd(); }

    /**

    • Withdraw cash */ public void getCash(){ currState.getCash(); }

    /**

    • Query balance */ public void queryBalance(){ currState.queryBalance(); }

    /**

    • Eject card */ public void ejectCard(){ currState.ejectCard(); }

    public String toString(){ return "Total cash ¥" + totalAmount; }

    /Omit大量 getter and setter here/ }

Everything is done; can't wait to test it

III. Running Example

First, implement a Test class:

package StatePattern;

import java.util.Scanner;

/**

  • Implement test class

  • @author ayqy / public class Test { public static void main(String[] args) { /Test data/ / Total in machine Account balance Withdrawal amount Password * 1000 500 200 123 * 1000 300 500 123 * 0 500 200 123 * */ try { test(1000, 500, 200, "123"); System.out.println("-------"); test(1000, 300, 500, "123"); System.out.println("-------"); test(0, 500, 200, "123"); } catch (Exception e) { System.out.println("Machine malfunctioned, repair request has been sent to maintenance party"); } }

    private static void test(int totalAmount, int balance, int amount, String pwd)throws Exception{ //Create ATM ATM atm; atm = new ATM(totalAmount, balance, amount, pwd); //Output initial state System.out.println(atm.toString()); atm.insertCard(); atm.submitPwd(); atm.getCash(); //Output final state System.out.println(atm.toString()); } }

The three test cases we designed (normal withdrawal, withdraw more than balance, no cash in machine) produce the following results:

IV. State Pattern vs Strategy Pattern

Remember the Strategy Pattern? Don't you think these two are very similar?

That's right; the class diagrams of these two patterns are exactly the same. Here's the explanation:

  • The state subject (owner) holds state objects; at runtime, it can change class behavior by dynamically specifying state objects
  • The strategy subject holds algorithm family objects; at runtime, it can change class behavior by dynamically selecting algorithms (strategies) from the algorithm family

In other words, both the State Pattern and Strategy Pattern support runtime polymorphism, and their implementation approach is composition + delegation. However, this doesn't mean these two patterns are the same, because their goals differ:

  • The State Pattern implements variable algorithm flow (i.e., state transitions; different states have different flows)
  • The Strategy Pattern implements selectable algorithm details (i.e., selecting algorithms within an algorithm family; an algorithm family contains multiple selectable algorithms)

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment