Thursday, February 5, 2009

State Pattern

The state pattern allows you to build code that mirrors the state diagram you might create for a component that has several mutually exclusive behaviors.   The code is built and run using the ANT build.xml file from this post and the result from running the code is shown in the ANT output that follows the Java code.  

This example is code that implements the behavior of a Gumball machine.   The Main class is used to illustrate that the machine cannot dispense gumballs unless the prerequisites have been met, i.e., there is candy in the machine, money has been inserted into the machine and the handle turned.   The first four steps in the Main class show the results of improperly attempting to get the candy and steps 5 through 8 show the behavior of the machine as the necessary operations required for the machine to dispense gumballs are performed.   The result of running this code is shown in the ANT output.
public class Main {
 
 public static void main(String[] args) {
  
  GumballMachine machine = new GumballMachine();
  System.out.println("1. turning handle:  " + machine.turnHandle());
  
  System.out.println("2. inserting money: " + machine.insertMoney());
  System.out.println("3. turning handle:  " + machine.turnHandle());
  System.out.println("4. taking gumball:  " + machine.takeGumballs());
  
  System.out.println("5. adding gumballs: " + machine.addGumballs());
  System.out.println("6. inserting money: " + machine.insertMoney());
  System.out.println("7. turning handle:  " + machine.turnHandle());
  System.out.println("8. taking gumball:  " + machine.takeGumballs());
 }
}
public class GumballMachine {
 GumballMachineState state;
 
 // constructor 
 public GumballMachine() {
  state = new HasNoGumballsState();
 }
 
 GumballMachine addGumballs() {
  state = state.addGumballs();
  return this;
 }
 GumballMachine insertMoney() {
  state = state.insertMoney();
  return this;
 }
 GumballMachine turnHandle() {
  state = state.turnHandle();
  return this;
 }
 GumballMachine takeGumballs() {
  state = state.takeGumballs();
  return this;
 }
 @Override
 public String toString() {
  return state.toString();
 }
}
public abstract class GumballMachineState { 
 GumballMachineState addGumballs() {
  return this;
 }
 GumballMachineState insertMoney() {
  return this;
 }
 GumballMachineState turnHandle() {
  return this;
 }
 GumballMachineState takeGumballs() {
  return this;
 }
}
public class HasNoGumballsState extends GumballMachineState {
 @Override
 HasNoMoneyState addGumballs() {
  return new HasNoMoneyState();
 }
 
 @Override
 public String toString() {
  return "Please add gumballs (" + this.getClass().getName() + ")";
 }
}
public class HasNoMoneyState extends GumballMachineState {
 @Override
 HandleNotTurnedState insertMoney() {
  return new HandleNotTurnedState();
 }

 @Override
 public String toString() {
  return "Please insert money (" + this.getClass().getName() + ")";
 }
}
public class HandleNotTurnedState extends GumballMachineState {
 @Override
 GumballsNotTakenState turnHandle() {
  return new GumballsNotTakenState();
 }
 
 @Override
 public String toString() {
  return "Please turn handle (" + this.getClass().getName() + ")";
 }
}
public class GumballsNotTakenState extends GumballMachineState {
 @Override
 HasNoMoneyState takeGumballs() {
  return new HasNoMoneyState();
 }
 
 @Override
 public String toString() {
  return "Please take gumballs (" + this.getClass().getName() + ")";
 }
}
statemachine> ant
Buildfile: build.xml

clean:
   [delete] Deleting directory /Users/greghelton/dev/src/java/StateMachine/build
   [delete] Deleting directory /Users/greghelton/dev/src/java/StateMachine/dist

compile:
    [mkdir] Created dir: /Users/greghelton/dev/src/java/StateMachine/build
    [javac] Compiling 7 source files to /Users/greghelton/dev/src/java/StateMachine/build

jar:
    [mkdir] Created dir: /Users/greghelton/dev/src/java/StateMachine/dist
      [jar] Building jar: /Users/greghelton/dev/src/java/StateMachine/dist/StateMachine.jar

run:
     [java] 1. turning handle:  Please add gumballs (HasNoGumballsState)
     [java] 2. inserting money: Please add gumballs (HasNoGumballsState)
     [java] 3. turning handle:  Please add gumballs (HasNoGumballsState)
     [java] 4. taking gumball:  Please add gumballs (HasNoGumballsState)
     [java] 5. adding gumballs: Please insert money (HasNoMoneyState)
     [java] 6. inserting money: Please turn handle (HandleNotTurnedState)
     [java] 7. turning handle:  Please take gumballs (GumballsNotTakenState)
     [java] 8. taking gumball:  Please insert money (HasNoMoneyState)

BUILD SUCCESSFUL
Total time: 0 seconds
statemachine>
The state pattern uses classes that act to block a change of state of the main component (here, the gumball machine) when the change is not allowed.   The gumball machine is not allowed to dispense candy when it has none, when no money has been deposited and when the handle has not been turned.  

Note that this rather complex behavior has been implemented without a single conditional statement.   Conditional logic can certainly be added if necessary.   The turnHandle() method of HandleNotTurnedState could 'return this' under some conditions and, under other conditions, 'return new GumballsNotTakenState()'.

The abstract class GumballMachineState acts to block all changes of state.   Each of the classes that extend GumballMachineState override one of its methods and return a new instance of a subclass of GumballMachineState.   These subclasses allow the state of the Gumball machine to be changed.