Chapter 23. The State Pattern

The State pattern allows you to have an object represent the state of your application and to switch application states by switching objects. For example, you can have an enclosing class switch between a number of related contained classes and pass method calls on to the current contained class. Design Patterns suggests that the State pattern switches between internal classes in such a way that the enclosing object appears to change its class. In Java, at least, this is a bit of an exaggeration, but the actual purpose to which the classes are put can change significantly.

Many programmers havwe had the experience of creating a class that performs slightly different computations or displays different information based on the arguments passed into the class. This often leads to some sort of switch or if-else statement inside of the class that determines which behavior to carry out. It is this inelegance that the State partern seeks to replace.

Sample Code

Let's consider the case of a drawing program similar to the one we develoed for the Momento class in Chapter 21. This program has toolbar buttoms for Selectr, Rectangle, Fill, Circle, and Clear, as shown in Figure 23.1.

A simple drawing program for illustrating the State pattern.

Figure 23.1. A simple drawing program for illustrating the State pattern.

Each toolbar button does something different when it is selected and the mouse is clicked or dragged across the screen. Thus the state of the graphical editor affects the behavior that the program should exhibit. This suggests some sort of design using the State pattern.

Initially, we might design our program like this, with a Mediator managing the actions of five command buttons, as shown in Figure 23.2.

One possible interaction between the classes needed to support the simple drawing program.

Figure 23.2. One possible interaction between the classes needed to support the simple drawing program.

However, this initial design puts the entire burden of maintaining the state of the program on the Mediator, when the main purpose of a Mediator is to coordinate activities between various controls, such as the buttons. Keeping the state of the buttons and the desired mouse activity inside the Mediator can make it unduly complicated as well as lead to a set of if or switch tests, which make the program difficult to read and maintain.

Further, this set of large, monolithic conditional statements might have to be repeated for each action that the Mediator interprets, such as mouseUp, mouseDrag, rightClick, and so on. This, too, makes the program very hard to read and maintain. Instead, let's analyze the expected behavior for each button.

  1. If the Pick button is selected, then clicking inside a drawing element should cause it to be highlighted or appear with handles. If the mouse is dragged and a drawing element is already selected, the element should move on thescreen.

  2. If the Rect button is selected, then clicking on the screen should cause a new rectangle drawing element to be created.

  3. If the Fill button is selected and a drawing element is already selected, then that element should be filled with the current color. If no drawing is selected, then clicking inside a drawing should cause it to be filled with the current color.

  4. If the Circle button is selected, then clicking on the screen should cause a new circle drawing element to be created.

  5. If the Clear button is selected, then all of the drawing elements are removed.

Several of these actions share some common threads. Four use the mouse click event to cause actions, and one uses the mouse drag event to cause an action. Thus we really want to create a system that can help us redirect these events based on which button is currently selected. Let's create a State object that handles mouse activities.

public class State {
    public void mouseDown(int x, int y) { }
    public void mouseUp(int x, int y) { }
    public void mouseDrag(int x, int y) { }
}

We include the mouseUp event in case we need it later. Because none of the cases described need all of these events, we give the base class empty methods rather than create an abstract base class. Then we create four derived State classes forPick, Rect, Circle, and Fill, and put instances of all of them inside a StateManager class that sets the current state and executes methods on that State object. In Design Patterns, this StateManager class is called a Context object, as illustrated in Figure 23.3.

A StateManager class that keeps track of the current state.

Figure 23.3. A StateManager class that keeps track of the current state.

A typical State object simply overrides those event methods that it must handle specially. For example, following is the complete Rectangle state object:

public class RectState extends State {
    private Mediator med;    //save the Mediator here
    public RectState(Mediator md) {
        med = md;
    }
    //create a new Rectangle where mouse clicks
    public void mouseDown(int x, int y) {
        med.addDrawing(new visRectangle(x, y));
    }
}

The RectState object tells the Mediator to add a rectangle to the drawing list. Similarly, the Circle state object tells the Mediator to add a circle to the drawing list.

public class CircleState extends State {
    private Mediator med;    //save the Mediator
    public CircleState(Mediator md) {
        med = md;
    }
    //draw a circle where the mouse clicks
    public void mouseDown(int x, int y) {
        med.addDrawing(new visCircle(x, y));
    }
}

The only tricky button is the Fill button because we have defined two actions forit:

  1. If an object is already selected, fill it.

  2. If the mouse is clicked inside an object, fill the object.

To carry out these tasks, we need to add the select method to the base State class. This method is called when each toolbar button is selected.

public class State {
    public void mouseDown(int x, int y) { }
    public void mouseUp(int x, int y) { }
    public void mouseDrag(int x, int y) { }
    public void select (Drawing d, Color c) { }
}

The Drawing argument is either the currently selected Drawing or null if none is selected, and the color is the current fill color. In this program, we have arbitrarily set the fill color to red. So the Fill state class is as follows:

public class FillState extends State {
    private Mediator med;           //state the Mediator
    private Color color;            //save the current color
    public FillState(Mediator md) {
        med = md;
    }
    //fill the drawing if selected
    public void select(Drawing d, Color c) {
        color = c;
        if (d!= null) {
            d.setFill(c);          //fill that drawing
        }
    }
    //fill the drawing if you click inside of one 
    public void mouseDown (int x, int y) {
        Vector drawings = med.getDrawings();
        for (int i = 0; i < drawings.size(); i++) {
            Drawing d = (Drawing)drawings.elementAt(i);
            if (d.contains(x, y))
                d.setFill(color);    //fill the drawing
        }
    }
}

Switching between States

Now that we have defined how each state behaves when mouse events are sent to it, we need to decide how the StateManager switches between states. We set the currentState variable to the state that is indicated by the selected button.

public class StateManager {
    private State            currentState;
    private RectState        rState;
    private ArrowState       aState;
    private CircleState      cState;
    private FillState        fState;

public StateManager(Mediator med) {
        rState = new RectState(med);
        cState = new CircleState(med);
        aState = new ArrowState(med);
        fState = new FillState(med);
        currentState = aState;
    }
    //these methods are called when the toolbar buttons
    //are selected
    public void setRect() {currentState = rState;}
    public void setCircle() {currentState = cState;}
    public void setFill() {currentState = fState;}
    public void setArrow() {currentState = aState;}

Note that in this version of the StateManager, we create an instance of each state during the constructor and copy the correct one into the state variable when the set methods are called. We also could create these states on demand. This might be advisable if a large number of states exists, each of which consumes a fair number of resources.

The remainder of the StateManager code calls the methods of whichever state object is current. This is the critical piece of code; there is no conditional testing. Instead, the correct state is already in place, and its methods are ready to be called.

public void mouseDown(int x, int y) {
    currentState.mouseDown(x, y);
}
public void mouseUp(int x, int y) {
    currentState.mouseUp(x, y);
}
public void mouseDrag(int x, int y) {
    currentState.mouseDrag(x, y);
}
public void select(Drawing d, Color c) {
    currentState.select(d, c);
    }
}

How the Mediator Interacts with the StateManager

Recall that it is clearer to separate the state management from the Mediator's button and mouse event management. The Mediator is the critical class, however, because it tells the StateManager when the current program state changes. The beginning part of the Mediator illustrates how this stage change takes place. Note that each button click calls one of these methods and changes the application's state. The remaining statements in each method simply turn off the other toggle buttons so that only one button at a time may be depressed.

public class Mediator {
    private boolean           startRect;
    private boolean           dSelected;
    private Vector            drawings;
    private Vector            undoList;
    private Rectbutton        rectButton;
    private FillButton        fillButton;
    private CircleButton      circButton;
    private PickButton        arrowButton;
    private JPanel            canvas;
    private Drawing           selectedDrawing;
    private StateManager      stMgr;

    public Mediator() {
        startRect =  false;
        dSelected =  false;
        drawings  =  new Vector();
        undoList  =  new Vector();
        stMgr     =  new StateManager(this);
    }
//--------------
    public void startRectangle() {
        stMgr.setRect();
        arrowButton.setSelected(false);
        circButton.setSelected(false);
        fillButton.setSelected(false);
    }
//--------------
    public void startCircle() {
        stMgr.setCircle();
        rectButton.setSelected(false);
        arrowButton.setSelected(false);
        fillButton.setSelected(false);
    }

These startXxx methods are called from the Execute method of each button as a Command object.

The class diagram for this program, illustrating the State pattern, is given in two parts. The State section is shown in Figure 23.4.

The StateManager and the Mediator.

Figure 23.4. The StateManager and the Mediator.

The interaction of the Mediator with the buttons is depicted in Figure 23.5.

Interaction between the buttons and the Mediator.

Figure 23.5. Interaction between the buttons and the Mediator.

State Transitions

The transition between states can be specified internally or externally. In the previous example, the Mediator tells the StateManager when to switch between states. However, it is also possible that each state can decide automatically what each successor state will be. For example, when a rectangle or circle drawing object is created, the program could automatically switch back to the arrow object State.

Mediators and the God Class

One real problem with programs that have this many objects interacting is that too much knowledge of the system is placed into the Mediator. This makes ita"god class." In the previous example, the Mediator communicates with thesixbuttons, the drawing list, and the StateManager. We can write this programanother way so that the button Command objects communicate with the StateManager, while the Mediator deals only with the buttons and the drawing list. Each button creates an instance of the required state and sends it to the StateManager. For example, when we select the Circle button, the following code executes:

public void Execute() {
    stateMgr.setState (new CircleState(med));  //new State
    med.startCircle();            //toggle buttons
}

Consequences of the State Pattern

Using the State pattern has the following consequences:

  1. The State pattern creates a subclass of a basic State object for each state that an application may have and switches between them as the application changes state.

  2. You don't need to have a long set of conditional if or switch statements associated with the various states because each state is encapsulated in a class.

  3. No variable specifies which state a program is in, so this approach reduces errors caused by programmers who forget to test this state variable.

  4. You could share State objects between several parts of an application, such as separate windows, as long as none of the State objects have specific instance variables. In this example, only the FillState class has an instance variable, and this could be easily rewritten to be an argument passed in each time.

  5. This approach generates a number of small class objects, but in the process, it simplifies and clarifies the program.

  6. In Java, all of the State objects must inherit from a common base class, and all must have common methods, although some of those methods may be empty. In other languages, the States can be implemented by function pointers with much less type checking and, of course, a greater chance of error.

Thought Questions

  1. Rewrite the StateManager to use a Factory pattern to produce the states on demand.

  2. While visual graphics programs provide obvious examples of State patterns, Java server programs can benefit by this approach. Outline a simple server that uses a State pattern.

Programs on the CD-ROM

Program Description

StateStateDraw.java

A simple drawing program that draws and moves rectangles and circles and allows you to fill the shapes with a solid color.

StateStateFactoryStateDraw.java

A drawing program that creates new instances of each state whenever the state changes. These states are created in the button Command objects rather than in the Mediator.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset