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.
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.
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.
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.
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.
If the Rect button is selected, then clicking on the screen should cause a new rectangle drawing element to be created.
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.
If the Circle button is selected, then clicking on the screen should cause a new circle drawing element to be created.
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 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:
If an object is already selected, fill it.
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 } } }
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); } }
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 interaction of the Mediator with the buttons is depicted in Figure 23.5.
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.
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 }
Using the State pattern has the following consequences:
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.
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.
No variable specifies which state a program is in, so this approach reduces errors caused by programmers who forget to test this state variable.
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.
This approach generates a number of small class objects, but in the process, it simplifies and clarifies the program.
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.
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. |