This article is lecture note from "Head First Design Patterns" by E_lisabeth Freeman and Kathy Sierra and CS course from Kookmin by Inkyu Kim
The State Pattern allows an object to alter its behaviour when its internal state changes. The object will appear to change its class.
It is very reasonable to understand State Pattern relating it to Finite-State-Machine.
The main idea is that, at any given moment, there's a finite number of states which a program can be in. Within any unique state, the program behaves differently, and the program can be switched from one state to another instantaneously. However, depending on a current state, the program may or may not switch to certain other states. These switching rules, called transitions are also finite and predetermined.
We have a gumball machine. Actually not of course.
Imagine we got asked of making gumball machine which has not just selling gumball also has some random game for giving additional gumball.
The original code was like this.
public class GumballMachine {
final static int SOLD_OUT = 0;
final static int NO_QUARTER = 1;
final static int HAS_QUARTER = 2;
final static int SOLD = 3;
int state = SOLD_OUT;
int count = 0;
public GumballMachine(int count) {
this.count = count;
if (count > 0) {
state = NO_QUARTER;
}
}
public void insertQuarter() {
if (state == HAS_QUARTER) {
System.out.println("You can't insert another quarter");
} else if (state == NO_QUARTER) {
state = HAS_QUARTER;
System.out.println("You inserted a quarter");
} else if (state == SOLD_OUT) {
System.out.println("You can't insert a quarter, the machine is sold out");
} else if (state == SOLD) {
System.out.println("Please wait, we're already giving you a gumball");
}
}
public void ejectQuarter() {
// ejectQuarter() here
}
public void turnCrank() {
// turnCrank() here
}
private void dispense() {
// dispense() here
}
public void refill(int numGumBalls) {
// refill() here
}
public String toString() {
// toString() here
}
}
We need to add new STATE which is winner state in random game. Then we need to add a new conditional in every single methods.. well, that's too many code to modify. Right?
Context stores a reference to one of the concrete state objects and delegates to it all state-specific work. The context communicates with the state object via the state interface. The context exposes a setter for passing it a new state object. The State interface declares the state-specific methods. These methods should make sense for all concrete states because you don't want some of your states to have useless methods that will never be called.
ConcreteStates provide their own implementations for the state-specific methods. To avoid duplication of similar code across multiple states, you may provide intermediate abstract classes that encapsulate some common behaviour. State object may store a backreference to the context object. Through this reference, the state can fetch any required info from the context object, as well as initiate state transitions.
Both context and concrete states can set the next state of the context and perform the actual state transition by replacing the state object linked to the context.
First let's create an interface for state, which all our states implement.
<State.java>
public interface State {
public void insertQuarter();
public void ejectQuarter();
public void turnCrank();
public void dispense();
public void refill();
}
Then we starts with NoQuarterState.
<NoQuarterState.java>
public class NoQuarterState implements State {
GumballMachine gumballMachine;
public NoQuarterState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
public void insertQuarter() {
System.out.println("You inserted a quarter");
gumballMachine.setState(gumballMachine.getHasQuarterState());
}
public void ejectQuarter() {
System.out.println("You haven't inserted a quarter");
}
public void turnCrank() {
System.out.println("You turned, but there's no quarter");
}
public void dispense() {
System.out.println("You need to pay first");
}
public void refill() { }
public String toString() {
return "waiting for quarter";
}
}
There is no if / elif / else like conditional statements.
Now look at the complete GumballMachine object.
<GumballMachine.java>
public class GumballMachine {
State soldOutState;
State noQuarterState;
State hasQuarterState;
State soldState;
State winnerState;
State state = soldOutState;
int count = 0;
public GumballMachine(int numberGumballs) {
soldOutState = new SoldOutState(this);
noQuarterState = new NoQuarterState(this);
hasQuarterState = new HasQuarterState(this);
soldState = new SoldState(this);
winnerState = new WinnerState(this);
this.count = numberGumballs;
if (numberGumballs > 0) {
state = noQuarterState;
}
}
public void insertQuarter() {
state.insertQuarter();
}
public void ejectQuarter() {
state.ejectQuarter();
}
public void turnCrank() {
state.turnCrank();
state.dispense();
}
void setState(State state) {
this.state = state;
}
void releaseBall() {
System.out.println("A gumball comes rolling out the slot...");
if (count > 0) {
count = count - 1;
}
}
int getCount() {
return count;
}
void refill(int count) {
this.count += count;
System.out.println("The gumball machine was just refilled; its new count is: " + this.count);
state.refill();
}
public State getState() {
return state;
}
public State getSoldOutState() {
return soldOutState;
}
public State getNoQuarterState() {
return noQuarterState;
}
public State getHasQuarterState() {
return hasQuarterState;
}
public State getSoldState() {
return soldState;
}
public State getWinnerState() {
return winnerState;
}
public String toString() {
StringBuffer result = new StringBuffer();
result.append("\nMighty Gumball, Inc.");
result.append("\nJava-enabled Standing Gumball Model #2004");
result.append("\nInventory: " + count + " gumball");
if (count != 1) {
result.append("s");
}
result.append("\n");
result.append("Machine is " + state + "\n");
return result.toString();
}
}
Now, for the actions. These are amazingly simple to implement now. We just delegate to the current state.
Note that we don't need an action method for dispense()
in GumballMachine
because it's just an internal action; a user can't ask the machine to dispense directly. But we do call dispense()
on the state object from the turnCrank()
method.
Use the State Pattern when you have a class polluted with massive conditionals that alter how the class behaves according to the current values of the class's fields. Then the pattern lets you extract branches of these conditionals into methods of corresponding state classes. While doing so, you can also clean temporary fields and helper methods involved in state-specific code out of your main class.
Use State Pattern when you have a lot of duplicate code aross similar states and transitions of a condition-based state machine. Then the pattern lets you compose hierarchies of state classes and reduce duplication by extracting common code into abstract base classes.