State

The State Pattern allows an object to alter its behaviour when its internal state changes. From the object user's point of view, it looks like the object changes the class from which it is instantiated. And, given that we could use composition this is true, in a way: we expose the behavior of a different class, accordingly to the state that we want to show.

The class that could have a number of different states is usually called Context. Any requests that is made on Context is delegated to the currently active state of the Context itself. Any concrete state of the Context is derived from a common interface, here name State, that defines all the method required by the Context. Since all the concrete state implements it, they are all interchangeable.

You can see that the class diagram for State Pattern is the same of the Strategy Pattern one. The difference is in their intent. In the State Pattern the behavior is delegated form the Context to the current State, and this is seen as an internal detail of the Context class, so usually the client doesn't know much about it. It could be thought as an alternative of putting a lot of conditional statements in the Context, using the different concrete states as an encapsulation of its changing behavior.

On the other side, with Strategy is usually the client that specify the actual state of the configurable object, so we can think this pattern as a flexible alternative to subclassing. If all this results a bit unclear, I would suggest you to get HFDP - Head First Design Patterns a fine and fun book on patterns implemented in Java.

The example that should help us in better understanding the use of this pattern is based on the implementation of a software for the management of a gumball machine.

The user puts a coin in it, turns a crank, and he expects to get back a gumball. It looks like a simple mechanism, but actually there are a few subtleties that have to be checked carefully. The gumball machine should react in a different way to an action from the user accordingly to its internal status (the number of gumballs currently available) and the history of the previous actions applied to the machine. Actually, our gumball machine is a simple finite state machine.

Once we have seen that, it comes quite natural to implement the gumball machine using the State Pattern. Let's think about the states our machine could have:
  • No Coin: this is the "normal" state. The machine has gumballs ready for selling, and it is waiting for clients.
  • Has Coin: we reach this state when the user puts a coin in the machine, causing the transition to here from the No Coin state.
  • Sold: we arrive here from Has Coin, when the user turns the crank. When we enter this state, the machine should internally cause a gumball to be delivered to the user. After that we should go to the No Coin state or, if no gumball is available anymore we should go to the Sold Out state.
  • Sold Out: no gumball is available. The only operation that makes sense in this state is the refill of the machine, that sends us to the No Coin status.
Besides, we create another state, Winner, that is a variation of Sold where two gumballs are delivered to the user, if available.

The transition among the states is ruled by a few operations:
  • The user inserts a coin: it makes sense only if we are in the No Coin state, and in this case it let us move to the Has Coin state.
  • The user asks his coin back: if we are in the Has Coin state, moves us back to the No Coin state. Otherwise it has no result.
  • The user turns the crank: only if we are in Has Coin we go to the Sold state, ready to deliver the gumball to the user.
  • The machine dispenses a gumball: the gumball machine, when the Sold state is reached, calls this operation.
Let's see a way to implement this bunch of states. First thing we create an abstract class with the default implementation for each state:
abstract class State { // 1
    void insertCoin() {
        System.out.println("Can't insert a coin now");
    }

    void ejectCoin() {
        System.out.println("Can't eject a coin now");
    }

    boolean turnCrank() { // 2
        System.out.println("No use in turning the crank now");
        return false;
    }

    void dispense() {
        System.out.println("Can't dispense a gum now");
    }
}
1. The class and the methods have package visibility, since these are internal details not available to the user.
2. The turnCrank() method returns a boolean, so that the machine would know when it makes sense to call dispense() - that is, only when the turning of the crank succeeded.

The machine is ready to dispense gumballs, we just wait for the user to put his coin in it, we are in the NoCoin state:
class StateNoCoin extends State {
    private GumballMachine machine;

    StateNoCoin(GumballMachine machine) {
        this.machine = machine;
    }

    @Override
    void insertCoin() {
        System.out.println("Coin inserted");
        machine.setState(machine.getStateHasCoin());
    }
}
When the coin is in the machine, the user could change his mind, asking for his coin back, or give us the OK for the gumball. We should randomly decide if we give him one or two gums for his money:
import java.util.Random;

class StateHasCoin extends State {
    private GumballMachine machine;
    private Random generator = new Random(System.currentTimeMillis());
    private static final int LUCKY = 10;

    StateHasCoin(GumballMachine machine) {
        this.machine = machine;
    }

    @Override
    void ejectCoin() { // 1
        System.out.println("Get your coin back");
        machine.setState(machine.getStateNoCoin());
    }

    @Override
    boolean turnCrank() { // 2
        System.out.println("Turning ...");
        if(generator.nextInt(LUCKY) == 0)
            machine.setState(machine.getStateWinner());
        else
            machine.setState(machine.getStateSold());

        // turning has success
        return true;
    }
}
1. The user has changed his mind, we get back to the No Coin status.
2. The turn of the crank could send us, randomly, to the Sold or to the Winner state. In any case we return true to the caller, to signal it that it makes sense now to call the delivery method.

The user has turned the crank, the gumball is sold, we had ho luck so we are about to get just one gumball:
class StateSold extends State {
    protected GumballMachine machine; // 1

    StateSold(GumballMachine machine) {
        this.machine = machine;
    }

    @Override
    void dispense() { // 2
        machine.releaseBall();
        if(machine.getGumballsNumber() == 0) {
            System.out.println("This was the last gumball!");
            machine.setState(machine.getStateSoldOut());
        } else {
            machine.setState(machine.getStateNoCoin());
        }
    }
}
1. We are going to extend StateSold for defining the Winner state, so the machine reference is protected, for being visible to the derived class, too.
2. After the machine releases the gumball, we change the state to No Coin, ready for another run, or to Sold Out, if no more gumballs are available.

We have been lucky, we are about to get two gumballs:
class StateWinner extends StateSold { // 1
    StateWinner(GumballMachine machine) {
        super(machine);
    }

    @Override
    void dispense() {
        machine.releaseBall();
        if(machine.getGumballsNumber() == 0) { // 2
            System.out.println("Damn, that was the last gumball");
            machine.setState(machine.getStateSoldOut());
        } else {
            System.out.println("Lucky guy! You get two gums for a coin.");
            super.dispense(); // 3
        }
    }
}
1. The Winner state is just a variation of the Sold state, so we design the StateWinner class as an StateSold extension.
2. After giving the user the gumball he paid for, we check if we actually have another one available. If not we change the state to Sold Out. The user won a second ball, but we can't give it to him.
3. To deliver the free gum the lucky user has won, we fallback to the superclass implementation for this method.

Out of gum, we can't do anything, until we get a refill:
class StateSoldOut extends State {
    @Override
    public void insertCoin() {
        System.out.println("Sorry: sold out - get back your coin"); // 1
    }
}
1. If a user put his money in the machine we tell him to get it back.

Our gumball machine starts in the No Coin (or in the Sold Out) state and change to different states accordingly to the action ruled by the class public interface. Notice that the change of state is done by the State-derived classes:
public class GumballMachine {
    private State stateNoCoin = new StateNoCoin(this);
    private State stateHasCoin = new StateHasCoin(this);
    private State stateSoldOut = new StateSoldOut();
    private State stateSold = new StateSold(this);
    private State stateWinner = new StateWinner(this);

    private int gumballs;
    private State current;

    public GumballMachine(int gumballs) {
        this.gumballs = gumballs;
        current = (gumballs > 0) ? stateNoCoin : stateSoldOut;
    }

    public int getGumballsNumber() {
        return gumballs;
    }

    State getStateHasCoin() {
        return stateHasCoin;
    }

    State getStateNoCoin() {
        return stateNoCoin;
    }

    State getStateSold() {
        return stateSold;
    }

    State getStateSoldOut() {
        return stateSoldOut;
    }

    State getStateWinner() {
        return stateWinner;
    }

    void setState(State state) {
        this.current = state;
    }

    void releaseBall() {
        System.out.println("You can get your gumball.");
        if(gumballs > 0)
        --gumballs;
    }

    public void insertCoin() {
        current.insertCoin();
    }

    public void ejectCoin() {
        current.ejectCoin();
    }

    public void turnCrank() {
        if(current.turnCrank())
            current.dispense();
    }

    public void refill(int moreGumballs) {
        System.out.println("Adding " + moreGumballs + " gumballs.");
        gumballs += moreGumballs;
        current = (gumballs > 0) ? stateNoCoin : stateSoldOut;
    }

    @Override
    public String toString() {
        return current.toString() + " [" + gumballs + ']';
    }
}
Here is a few lines to do a smoke test of the gumball machine:
GumballMachine gm = new GumballMachine(5);
System.out.println(gm);

gm.insertCoin();
gm.turnCrank();
System.out.println(gm);

gm.insertCoin(); // just to mess up a bit
while(gm.getGumballsNumber() > 0) {
    gm.insertCoin();
    gm.turnCrank();
}

System.out.println(gm);
gm.insertCoin();
gm.turnCrank();
System.out.println(gm);

gm.refill(2);
System.out.println(gm);
while(gm.getGumballsNumber() > 0) {
    gm.insertCoin();
    gm.turnCrank();
}

No comments:

Post a Comment