Chapter 28. The State Pattern

The State pattern is used when you want to have an object represent the state of your application and switch application states by switching objects. For example, you could 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 C#, at least, this is a bit of an exaggeration, but the actual purpose to which the classes are applied can change significantly.

Many programmers have 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 frequently leads to some types of select case or if-else statements inside the class that determine which behavior to carry out. It is this inelegance that the State pattern seeks to replace.

Sample Code

Let’s consider the case of a drawing program similar to the one we developed for the Memento class. Our program will have toolbar buttons for Select, Rectangle, Circle, Fill, Undo, and Clear. We show this program in Figure 28-1.

Figure 28-1. A simple drawing program for illustrating the State pattern

image

Each one of the tool buttons does something rather different when it is selected, and you click or drag your mouse across the screen. Thus, the state of the graphical editor affects the behavior 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 28-2.

Figure 28-2. One possible interaction between the classes needed to support the simple drawing program

image

However, this initial design puts the entire burden of maintaining the state of the program on the Mediator, and we know that 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 result in a set of If or Select tests that can make the program difficult to read and maintain.

Further, this set of large, monolithic conditional statements might have to be repeated for each action the Mediator interprets, such as mouseUp, mouseDrag, rightClick, and so forth. This makes the program very hard to read and maintain.

Instead, let’s analyze the expected behavior for each of the buttons.

  1. If the Select button is selected, 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 the screen.
  2. If the Rect button is selected, 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, that element should be filled with the current color. If no drawing is selected, then clicking inside a drawing should fill it with the current color.
  4. If the Circle button is selected, clicking on the screen should cause a new circle drawing element to be created.
  5. If the Clear button is selected, all the drawing elements are removed.

There are some common threads among several of these actions that we should explore. Four of them use the mouse click event to cause actions. 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 consider creating a State object that handles mouse activities.


public class State    {
      //keeps state of each button
      protected Mediator med;
      public State(Mediator md) {
             med = md;    //save reference to mediator
      }
      public virtual void mouseDown(int x, int y) {}
      public virtual void mouseUp(int x, int y) {    }
      public virtual void mouseDrag(int x, int y) {}
}

Note that we are creating an actual class here with empty methods rather than an interface. This allows us to derive new State objects from this class and only have to fill in the mouse actions that actually do anything for that case. Then we’ll create four derived State classes for Pick, 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 referred to as a Context. This object is illustrated in Figure 28-3.

Figure 28-3. A StateManager class that keeps track of the current state

image

A typical State object simply overrides those event methods that it must handle specially. For example, this is the complete Rectangle state object. Note that since it only needs to respond to the mouseDown event, we don’t have to write any code at all for the other events.


public class RectState :State    {
      public RectState(Mediator md) :base (md) {}
      //-----
      public override void mouseDown(int x, int y) {
             VisRectangle vr = new VisRectangle(x, y);
             med.addDrawing (vr);
      }
}

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


public class CircleState : State {
      public CircleState(Mediator md) :base (md){ }
      //-----
      public override void mouseDown(int x, int y) {
             VisCircle c = new VisCircle(x, y);
             med.addDrawing (c);
      }
}

The only tricky button is the Fill button because we have defined two actions for it.

  1. If an object is already selected, fill it.
  2. If the mouse is clicked inside an object, fill that one.

In order to carry out these tasks, we need to add the selectOne method to our base State interface. This method is called when each tool button is selected.


public class State    {
      //keeps state of each button
      protected Mediator med;
      public State(Mediator md) {
             med = md;    //save reference to mediator
      }
      public virtual void mouseDown(int x, int y) {}
      public virtual void mouseUp(int x, int y) {}
      public virtual void mouseDrag(int x, int y) {}
      public virtual void selectOne(Drawing d) {}
}

The Drawing argument is either the currently selected Drawing or null if none is selected. In this simple program, we have arbitrarily set the fill color to red, so our Fill state class becomes the following.


public class FillState : State  {
      public FillState(Mediator md): base(md) { }
      //-----
      public override void mouseDown(int x, int y) {
             //Fill drawing if you click inside one
             int i = med.findDrawing(x, y);
             if (i >= 0) {
                    Drawing d = med.getDrawing(i);
                    d.setFill(true);  //fill drawing
             }
      }
      //-----
      public override void selectOne(Drawing d) {
             //fill drawing if selected
             d.setFill (true);
      }
}

Switching between States

Now that we have defined how each state behaves when mouse events are sent to it, we need to examine how the StateManager switches between states. We create an instance of each state, and then we simply set the currentState variable to the state indicated by the button that is selected.


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

      public StateManager(Mediator med)              {
             //create an instance of each state
             rState = new RectState(med);
             cState = new CircleState(med);
             aState = new ArrowState(med);
             fState = new FillState(med);
             //and initialize them
             //set default state
             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. It would also be possible to create these states on demand. This might be advisable if there are a large number of states that each consume a fair number of resources.

The remainder of the StateManager code simply calls the methods of whichever state object is current. This is the critical piece—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 selectOne(Drawing d) {
       currentState.selectOne (d);
}

How the Mediator Interacts with the StateManager

We mentioned 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, since it tells the StateManager when the current program state changes. The beginning part of the Mediator illustrates how this state change takes place. Note that each button click calls one of these methods and changes the state of the application. The remaining statements in each method simply turn off the other toggle buttons so only one button at a time can be depressed.


public class Mediator {
      private bool startRect;
      private int selectedIndex;
      private RectButton rectb;
      private bool dSelected;
      private ArrayList drawings;
      private ArrayList undoList;
      private RectButton rButton;
      private FillButton filButton;
      private CircleButton circButton;
      private PickButton arrowButton;
      private PictureBox canvas;
      private int selectedDrawing;
      private StateManager stMgr;
      //-----
      public Mediator(PictureBox pic)          {
             startRect = false;
             dSelected = false;
             drawings = new ArrayList();
             undoList = new ArrayList();
             stMgr = new StateManager(this);
             canvas = pic;
             selectedDrawing = -1;
      }
      //-----
      public void startRectangle() {
             stMgr.setRect();
             arrowButton.setSelected(false);
             circButton.setSelected(false);
             filButton.setSelected(false);
      }
      //-----
      public void startCircle() {
             stMgr.setCircle();
             rectb.setSelected(false);
             arrowButton.setSelected(false);
             filButton.setSelected(false);
      }

The ComdToolBarButton

In the discussion of the Memento pattern, we created a series of button Command objects paralleling the toolbar buttons and keep them in a Hashtable to be called when the toolbar button click event occurs. However, a powerful alternative is to create a ComdToolBarButton class that implements the Command interface as well as being a ToolBarButton. T hen, each button can have an Execute method that defines its purpose. Here is the base class.


public class ComdToolBarButton : ToolBarButton , Command   {
      private System.ComponentModel.Container components = null;
      protected Mediator med;
      protected  bool selected;
      public ComdToolBarButton(string caption, Mediator md)
      {
             InitializeComponent();
             med = md;
             this.Text =caption;
      }
      //------
      public void setSelected(bool b) {
             selected = b;
             if(!selected)
                    this.Pushed =false;
      }
      //-----
      public virtual void Execute() {
      }

Note that the Execute method is empty in this base class, but it is virtual, so we can override it in each derived class. In this case, we cannot use the IDE to create the toolbar but can simply add the buttons to the toolbar programmatically.


private void init() {
             //create a Mediator
             med = new Mediator(pic);
             //create the buttons
             rctButton = new RectButton(med);
             arowButton = new PickButton(med);
             circButton = new CircleButton(med);
             flButton = new FillButton(med);
             undoB = new UndoButton(med);
             clrb = new ClearButton(med);
             //add the buttons into the toolbar
             tBar.Buttons.Add(arowButton);
             tBar.Buttons.Add(rctButton);
             tBar.Buttons.Add(circButton);
             tBar.Buttons.Add(flButton);
             //include a separator
             ToolBarButton sep =new ToolBarButton();
             sep.Style = ToolBarButtonStyle.Separator;
             tBar.Buttons.Add(sep);
             tBar.Buttons.Add(undoB);
             tBar.Buttons.Add(clrb);
      }

Then we can catch all the toolbar button click events in a single method and call each button’s Execute method.


private void tBar_ButtonClick(object sender,
             ToolBarButtonClickEventArgs e) {
      Command comd = (Command)e.Button ;
      comd.Execute ();
}

The class diagram for this program illustrating the State pattern in this application is illustrated in two parts. The State section is shown in Figure 28-4.

Figure 28-4. The StateManager and the Mediator

image

The connection of the Mediator to the buttons is shown in Figure 28-5.

Figure 28-5. Interaction between the buttons and the Mediator

image

Handling the Fill State

The Fill State object is only slightly more complex because we have to handle two cases. The program will fill the currently selected object if one exists or fill the next one that you click on. This means there are two State methods we have to fill in for these two cases, as we see here.


public class FillState : State   {
      public FillState(Mediator md): base(md) { }
      //-----
      public override void mouseDown(int x, int y) {
             //Fill drawing if you click inside one
             int i = med.findDrawing(x, y);
             if (i >= 0) {
                    Drawing d = med.getDrawing(i);
                    d.setFill(true);  //fill drawing
             }
      }
      //-----
      public override void selectOne(Drawing d) {
             //fill drawing if selected
             d.setFill (true);
      }
}

Handling the Undo List

Now we should be able to undo each of the actions we carry out in this drawing program, and this means that we keep them in an undo list of some kind. These are the actions we can carry out and undo.

  1. Creating a rectangle
  2. Creating a circle
  3. Moving a rectangle or circle
  4. Filling a rectangle or circle

In our discussion of the Memento pattern, we indicated that we would use a Memento object to store the state of the rectangle object and restore its position from that Memento as needed. This is generally true for both rectangles and circles, since we need to save and restore the same kind of position information. However, the addition of rectangles or circles and the filling of various figures are also activities we want to be able to undo. And, as we indicated in our previous Memento discussion, the idea of checking for the type of object in the undo list and performing the correct undo operation is a really terrible idea.


//really terrible programming approach
    object obj = undoList[last];
    try{
           Memento mem = (Memento)obj;
           remove(mem);
    }
    catch (Exception) {
           removeDrawing();
    }

Instead, let’s define the Memento as an interface.


public interface Memento   {
      void restore();
}

Then all of the objects we add into the undo list will implement the Memento interface and will have a restore method that performs some operation. Some kinds of Mementos will save and restore the coordinates of drawings, and others will simply remove drawings or undo fill states.

First, we will have both our circle and rectangle objects implement the Drawing interface.


public interface Drawing  {
      void setSelected(bool b);
      void draw(Graphics g);
      void move(int xpt, int ypt );
      bool contains(int x,int y);
      void setFill(bool b);
      CsharpPats.Rectangle getRects();
      void setRects(CsharpPats.Rectangle rect);
}

The Memento we will use for saving the state of a Drawing will be similar to the one we used in the Memento chapter, except that we specifically make it implement the Memento interface.


public class DrawMemento : Memento     {
      private int x, y, w, h;
      private Rectangle  rect;
      private Drawing visDraw;
      //------
      public DrawMemento(Drawing d)    {
              visDraw = d;
             rect = visDraw.getRects ();
             x = rect.x;
             y = rect.y ;
             w = rect.w;
             h = rect.h;
      }
      //-----
      public void restore() {
              //restore the state of a drawing object
             rect.x = x;
             rect.y = y;
             rect.h = h;
             rect.w = w;
             visDraw.setRects( rect);
      }
      }

Now for the case where we just want to remove a drawing from the list to be redrawn, we create a class to remember that index of that drawing and remove it when its restore method is called.


public class DrawInstance :Memento {
      private int intg;
      private Mediator med;
      //-----
      public DrawInstance(int intg, Mediator md)    {
             this.intg = intg;
             med = md;
      }
      //-----
      public int integ {
             get { return intg;               }
      }
      //-----
      public void restore() {
             med.removeDrawing(intg);
      }
}

We handle the FillMemento in just the same way, except that its restore method turns off the fill flag for that drawing element.


public class FillMemento : Memento     {
      private int index;
      private Mediator med;
      //-----
      public FillMemento(int dindex, Mediator md) {
             index = dindex;
             med = md;
      }
      //-----
      public void restore() {
             Drawing d = med.getDrawing(index);
             d.setFill(false);
      }
}

The VisRectangle and VisCircle Classes

We can take some useful advantage of inheritance in designing our VisRectangle and VisCircle classes. We make VisRectangle implement the Drawing interface and then have VisCircle inherit from VisRectangle. This allows us to reuse the setSelected, setFill, and move methods and the rects properties. In addition, we can split off the drawHandle method and use it in both classes. Our new VisRectangle class looks like this.


public class VisRectangle : Drawing    {
      protected int x, y, w, h;
      private const int SIZE=30;
      private CsharpPats.Rectangle rect;
      protected bool selected;
      protected bool filled;
      protected Pen bPen;
      protected SolidBrush bBrush, rBrush;
      //-----
      public VisRectangle(int xp, int yp)            {
             x = xp;                    y = yp;
             w = SIZE;                  h = SIZE;
             saveAsRect();
             bPen = new Pen(Color.Black);
             bBrush = new SolidBrush(Color.Black);
             rBrush = new SolidBrush (Color.Red );
      }
      //-----
      //used by Memento for saving and restoring state
      public CsharpPats.Rectangle getRects() {
             return rect;
      }
      //-----
      public void setRects(CsharpPats.Rectangle value) {
                    x=value.x;               y=value.y;
                    w=value.w;               h=value.h;
                    saveAsRect();
      }
      //------
      public void setSelected(bool b) {
             selected = b;
      }
      //-----
      //move to new position
      public void move(int xp, int yp) {
             x = xp; y = yp;
             saveAsRect();
      }
      //-----
      public virtual void draw(Graphics g) {
             //draw rectangle
             g.DrawRectangle(bPen, x, y, w, h);
             if(filled)
                    g.FillRectangle (rBrush, x,y,w,h);
             drawHandles(g);
      }
      //-----
      public void drawHandles(Graphics g) {
      if (selected) {   //draw handles
             g.FillRectangle(bBrush, x + w / 2, y - 2, 4, );
             g.FillRectangle(bBrush, x - 2, y + h / 2, 4, );
             g.FillRectangle(bBrush, x + (w / 2),
                    y + h - 2, 4, 4);
             g.FillRectangle(bBrush, x + (w - 2),
             y + (h / 2), 4, 4);
      }
      }
      //-----
      //return whether point is inside rectangle
      public bool contains(int x, int y) {
             return rect.contains (x, y);
      }
      //------
      //create Rectangle object from new position
      protected void saveAsRect() {
             rect = new CsharpPats.Rectangle (x,y,w,h);
      }
      public void setFill(bool b) {
             filled = b;
      }

However, our VisCircle class only needs to override the draw method and have a slightly different constructor.


public class VisCircle : VisRectangle   {
      private int r;
      public VisCircle(int x, int y):base(x, y)           {
             r = 15; w = 30; h = 30;
             saveAsRect();
      }
      //-----
      public override void draw(Graphics g) {
             if (filled) {
                    g.FillEllipse(rBrush, x, y, w, h);
             }
      g.DrawEllipse(bPen, x, y, w, h);
      if (selected ){
             drawHandles(g);
             }
      }
}

Note that since we have made the x, y, and filled variables protected, we can refer to them in the derived VisCircle class without declaring them at all.

Mediators and the God Class

One big problem with programs when this many objects are interacting is putting too much knowledge of the system into the Mediator so it becomes a “god class.” In the preceding example, the Mediator communicates with the six buttons, the drawing list, and the StateManager. We could write this program another way so that the button Command objects communicate with the StateManager and the Mediator only deals with the buttons and the drawing list. Here, each button creates an instance of the required state and sends it to the StateManager. This we will leave as an exercise for the reader.

Consequences of the State Pattern

  1. The State pattern creates a subclass of a basic State object for each state an application can have and switches between them as the application changes between states.
  2. You don’t need to have a long set of conditional if or switch statements associated with the various states, since each is encapsulated in a class.
  3. Since there is no variable anywhere that specifies which state a program is in, this approach reduces errors caused by programmers forgetting 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 simplifies and clarifies the program.
  6. In C#, all of the States must implement a common interface, and they must thus all have common methods, although some of those methods can be empty. In other languages, the states can be implemented by function pointers with much less type checking and, of course, greater chance of error.

State Transitions

The transition between states can be specified internally or externally. In our 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.

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, server programs can benefit from this approach. Outline a simple server that uses a State pattern.

Program on the CD-ROM

image

..................Content has been hidden....................

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