Design Patterns: The State Pattern in Java
by Riley MacDonald, October 29, 2017

The state pattern is very similar to the Strategy Pattern as it allows the behavior of objects to be altered at runtime. Objects behavior will be based on its internal state. The behavior of these states are broken down into separate classes.

Example Problem
I’ll use an example implementation of a pinball machine. The machine accepts user actions insertQuarter(), returnQuarter and startButtonPressed(). These are the represented as methods. The machine has three states NO_CREDIT, HAS_CREDIT and GAME_STARTED. This is what a simple implementation might look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class PinballMachine {
    // States
    final static int NO_CREDIT = 0;
    final static int HAS_CREDIT = 1;
    final static int GAME_STARTED = 2;
 
    int state = NO_CREDIT;
    int credit = 0;
 
    public PinballMachineBad() { }
 
    // Action methods
    public void insertQuarter() {
        credit += 1;
        if (state == NO_CREDIT) state = HAS_CREDIT;
    }
 
    public void returnQuarter() {
        if (state == HAS_CREDIT && credit >= 1) {
            // doEject();
            state = NO_CREDIT;
            credit -= 1;
        } else if (state == GAME_STARTED) {
            System.out.println("Please wait for the game to end before requesting a quarter return.");
        } else {
            System.out.println("No quarter inserted, cannot return.");
        }
    }
 
    public void startButtonPressed() {
        if (state == HAS_CREDIT) {
            state = GAME_STARTED;
            credit -= 1;
            // startGame();
        } else {
            System.out.println("No credits, please insert a quarter.");
        }
    }
}

Reviewing the example
This class should work as expected. The user should receive an additional credit for each quarter inserted. Pressing the start button should use a credit and start the game (details omitted). Pressing the return quarter button should return a quarter (if present) and subtract a credit from the machine (I realize this isn’t a great real world example, most pinball machines won’t operate this way). This is just a simple implementation and already the code is a bit messy. What happens when you want to add a new state? Most pinball machines have a chance to win a free credit at the game over screen. Adding a new state such as this will involve modifying some or all the if statements increasing their complexity and lowering readability.

Implementing the State Pattern
To implement the state pattern the existing state logic (from above) will be encapsulated into state objects in their own classes. Let’s start by adding the “action” methods to an interface:

1
2
3
4
5
public interface PinballState {
    public void insertQuarter();
    public void returnQuarter();
    public void startButtonPressed();
}

PinballMachine will now be responsible for owning the current PinballState. Let’s update PinballMachine to get and set the PinballState and credits:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class PinballMachine {
    PinballState noCreditState;
    PinballState creditState;
    PinballState gameStartedState;
 
    PinballState state;
    int credits;
 
    public PinballMachine() {
        noCreditState = new NoCreditState(this);
        creditState = new CreditState(this);
        gameStartedState = new GameStartedState(this);
    }
 
    // Actions
    public void insertQuarter() { state.insertQuarter(); }
    public void returnQuarter() { state.returnQuarter(); }
    public void startButtonPressed() { state.startButtonPressed(); }
 
    // States
    public PinballState getNoCreditState() { return noCreditState; }
    public PinballState getCreditState() { return creditState; }
    public PinballState getGameStartedState() { return gameStartedState; }
    public void setPinballState(PinballState pinballState) { state = pinballState; }
 
    // Credits
    public void setCredits(int credits) { this.credits = credits; }
    public int getCredits() { return credits; }
}

Notice all the PinballMachine “actions” now just invoke the appropriate state methods. PinballMachine is no longer concerned about what PinballState the machine is in, the PinballState implementations handle that. Now we can add the PinballState implementations for NoCreditState, CreditState and GameStartedState:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// the machine has no credits
public class NoCreditState implements PinballState {
    private PinballMachine pinballMachine;
 
    public NoCreditState(PinballMachine pinballMachine) {
        this.pinballMachine = pinballMachine;
    }
 
    @Override
    public void insertQuarter() {
        // the machine has no credits, add one and set the new state
        pinballMachine.setCredits(1);
        pinballMachine.setPinballState(pinballMachine.getCreditState());
    }
 
    // the machine has no credits, just print an error
    @Override public void returnQuarter() { printNoCreditError(); }
    @Override public void startButtonPressed() { printNoCreditError(); }
 
    private void printNoCreditError() {
        System.out.println("0 Credits. Please insert a quarter to play.");
    }
}
 
// the machine has credit(s)
public class CreditState implements PinballState {
    private PinballMachine pinballMachine;
 
    public CreditState(PinballMachine pinballMachine) {
        this.pinballMachine = pinballMachine;
    }
 
    @Override
    public void insertQuarter() {
        updatePinballMachineCreditsBy(1);
    }
 
    @Override
    public void returnQuarter() {
        updatePinballMachineCreditsBy(-1);
 
        if (pinballMachine.getCredits() == 0)
            pinballMachine.setPinballState(pinballMachine.getNoCreditState());
    }
 
    @Override
    public void startButtonPressed() {
        updatePinballMachineCreditsBy(-1);
        pinballMachine.setPinballState(pinballMachine.getGameStartedState());
    }
 
    private void updatePinballMachineCreditsBy(int creditValue) {
        pinballMachine.setCredits(pinballMachine.getCredits() + creditValue);
    }
}
 
public class GameStartedState implements PinballState {
    private PinballMachine pinballMachine;
 
    public GameStartedState(PinballMachine pinballMachine) {
        this.pinballMachine = pinballMachine;
    }
 
    @Override
    public void insertQuarter() {
        pinballMachine.setCredits(pinballMachine.getCredits() + 1);
    }
 
    @Override public void returnQuarter() { printGameInProgressError(); }
 
    @Override public void startButtonPressed() {
        if (pinballMachine.getCredits() >= 1) {
            pinballMachine.setCredits(pinballMachine.getCredits() - 1);
            // pinballMachine.addPlayer(); omitting details
        } else printGameInProgressError();
    }
 
    private void printGameInProgressError() {
        System.out.println("Game in progress, please wait until the game is finished.");
    }
}

Summary
All of the PinballMachine logic is now encapsulated in well described classes. Each PinballState describes one state. State classes can be shared across context instances if needed. Following the “one class, one responsibility” design principle can result in several classes containing somewhat duplicated code. An approach to reduce duplicated code would be to use an abstract instead of an interface. The pinball machine example doesn’t really have any shared code so I just used an interface.

Game Over?
I’ve left out the GameOver state. This is important because it should reset the machine to the appropriate state when the game has ended. Let’s see how adding a new PinballState looks after implementing the State Pattern:

1
2
3
4
// PinballMachine.java new method
public void gameOver() {
    setPinballState(new GameOverState(this));
}

Instead of storing GameOverState I’m creating it on the fly. This is because GameOverState invokes match(), generates a random number and sets a new PinballState based on the credit count.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class GameOverState implements PinballState {
    private PinballMachine pinballMachine;
 
    public GameOverState(PinballMachine pinballMachine) {
        this.pinballMachine = pinballMachine;
 
        match();
    }
 
    @Override
    public void insertQuarter() {
        pinballMachine.setCredits(pinballMachine.getCredits() + 1);
    }
 
    // Will never happen, the constructor invokes match and sets a new state
    @Override public void returnQuarter() { }
    @Override public void startButtonPressed() { }
 
    private void match() {
        int range = (10 - 1) + 1;
        int match = (int)(Math.random() * range) + 1;
        if (match == 5) {
            pinballMachine.setCredits(pinballMachine.getCredits() + 1);
            System.out.println("Match!");
        }
 
        pinballMachine.setPinballState(
                pinballMachine.getCredits() >= 1 ?
                        pinballMachine.getCreditState() :
                        pinballMachine.getNoCreditState()
        );
    }
}
Open the comment form

Leave a comment:

Comments will be reviewed before they are posted.

User Comments:

Be the first to leave a comment on this post!