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 VB, 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.
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, Fill, Circle, and Clear. We show this program in Figure 29-1.
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 29-2. 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 leading to a set of If or Select tests that make the program difficult to read and maintain.
Figure 29-2. One possible interaction between the classes needed to support the simple drawing program
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.
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.
If the Rect button is selected, 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, 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.
If the Circle button is selected, clicking on the screen should cause a new circle drawing element to be created.
If the Clear button is selected, all the drawing elements are removed.
There are some common threads among several of these actions 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.
'Interface State Public Sub mouseDown(X As Integer, Y As Integer) End Sub '----- Public Sub mouseUp(X As Integer, Y As Integer) End Sub '----- Public Sub mouseDrag(X As Integer, Y As Integer) End Sub
We'll include the mouseUp event in case we need it later. 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 29-3.
A typical State object simply overrides (in VB6, implements and fills out) those event methods that it must handle specially. For example, this is the complete Rectangle state object.
'Class RectState Implements State Private med As Mediator Public Sub init(md As Mediator) Set med = md End Sub '----- Private Sub State_mouseDown(X As Integer, Y As Integer) Dim vr As New VisRectangle vr.init X, Y med.addDrawing vr End Sub '----- Private Sub State_mouseDrag(X As Integer, Y As Integer) End Sub '----- Private Sub State_mouseUp(X As Integer, Y As Integer) End Sub
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.
'Class CircleState Implements State Private med As Mediator '----- Public Sub init(md As Mediator) Set med = md End Sub '----- Private Sub State_mouseDown(X As Integer, Y As Integer) Dim c As visCircle Set c = New visCircle c.init X, Y med.addDrawing c End Sub '----- Private Sub State_mouseDrag(X As Integer, Y As Integer) End Sub '----- Private Sub State_mouseUp(X As Integer, Y As Integer) End Sub
The only tricky button is the Fill button because we have defined two actions for it.
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.
'Interface State Public Sub mouseDown(X As Integer, Y As Integer) End Sub '----- Public Sub mouseUp(X As Integer, Y As Integer) End Sub '----- Public Sub mouseDrag(X As Integer, Y As Integer) End Sub '----- Public Sub selectOne(d As Drawing) End Sub
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.
'Class FillState Implements State Private med As Mediator Private color As ColorConstants '----- Public Sub init(md As Mediator) Set med = md color = vbRed End Sub '----- Private Sub State_mouseDown(X As Integer, Y As Integer) Dim drawings As Collection Dim i As Integer Dim d As Drawing 'Fill drawing if you click inside one Set drawings = med.getDrawings() For i = 1 To drawings.Count Set d = drawings(i) If d.contains(X, Y) Then d.setFill color 'fill drawing End If Next i End Sub '----- Private Sub State_mouseDrag(X As Integer, Y As Integer) End Sub '----- Private Sub State_mouseUp(X As Integer, Y As Integer) End Sub '----- Private Sub State_selectOne(d As Drawing) 'Fill drawing if selected d.setFill color 'fill that drawing End Sub
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.
'Class StateManager Private currentState As State Private rState As RectState Private aState As ArrowState Private cState As CircleState Private fState As FillState '----- Public Sub init(med As Mediator) 'create an instance of each state Set rState = New RectState Set cState = New CircleState Set aState = New ArrowState Set fState = New FillState 'and initialize them rState.init med cState.init med aState.init med fState.init med 'set default state Set currentState = aState End Sub
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 state manager 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 Sub mouseDown(X As Integer, Y As Integer) currentState.mouseDown X, Y End Sub '----- Public Sub mouseUp(X As Integer, Y As Integer) currentState.mouseUp X, Y End Sub '----- Public Sub mouseDrag(X As Integer, Y As Integer) currentState.mouseDrag X, Y End Sub '----- Public Sub selectOne(d As Drawing, c As ColorConstants) currentState.selectOne d End Sub
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.
'Class Mediator Private startRect As Boolean Private selectedIndex As Integer Private rectb As RectButton Private dSelected As Boolean Private drawings As Collection Private undoList As Collection Private rbutton As RectButton Private filbutton As FillButton Private circButton As CircleButton Private arrowButton As PickButton Private canvas As PictureBox Private selectedDrawing As Integer Private stmgr As StateManager '----- Public Sub init(Pic As PictureBox) startRect = False dSelected = False Set drawings = New Collection Set undoList = New Collection Set stmgr = New StateManager stmgr.init Me Set canvas = Pic End Sub '----- Public Sub startRectangle() stmgr.setRect arrowButton.setSelected (False) circButton.setSelected (False) filbutton.setSelected (False) End Sub '----- Public Sub startCircle() Dim st As State stmgr.setCircle rectb.setSelected False arrowButton.setSelected False filbutton.setSelected False End Sub
As we did in the discussion of the Memento pattern, we create a series of button Command objects paralleling the toolbar buttons and keep them in an array to be called when the toolbar button click event occurs.
Private Sub Form_Load() Set buttons = New Collection 'create an instance of the Mediator Set med = New Mediator med.init Pic 'Create the button command objects 'give each of them access to the Mediator Set pickb = New PickButton pickb.init med, tbar.buttons(1) Set rectb = New RectButton rectb.init med, tbar.buttons(2) Set filb = New FillButton filb.init med, tbar.buttons(3) Set cb = New CircleButton cb.init med, tbar.buttons(4) Set clrb = New ClearButton clrb.init med Set undob = New UndoButton undob.init med 'keep a Collection of the button Command objects buttons.Add pickb buttons.Add rectb buttons.Add filb buttons.Add cb buttons.Add undob buttons.Add clrb End Sub
These Execute methods in turn call the preceding startXxx methods.
Private Sub tbar_ButtonClick(ByVal Button As MSComctlLib.Button) Dim i As Integer Dim cmd As Command 'find out which button was clicked i = Button.index 'get that command object Set cmd = buttons(i) cmd.Execute 'and execute it End Sub
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 29-4.
The connection of the Mediator to the buttons is shown in Figure 29-5.
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.
'Class FillState Implements State Private med As Mediator '----- Public Sub init(md As Mediator) Set med = md End Sub '----- Private Sub State_mouseDown(x As Integer, y As Integer) Dim drawings As Collection Dim i As Integer Dim d As Drawing 'Fill drawing if you click inside one i = med.findDrawing(x, y) If i > 0 Then Set d = med.getDrawing(i) d.setFill True 'fill drawing End If End Sub '----- Private Sub State_mouseDrag(x As Integer, y As Integer) End Sub '----- Private Sub State_mouseUp(x As Integer, y As Integer) End Sub '----- Private Sub State_selectOne(d As Drawing) 'Fill drawing if selected d.setFill True 'fill that drawing End Sub
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.
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 the 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 Set obj = undoList(undoList.count) undoList.remove undoList.count 'and remove it If Not (TypeOf obj Is Memento) Then drawings.remove drawings.count Else obj.restore End If
Instead, let's define the Memento as an interface.
'Interface Memento Public Sub init(d As Drawing) End Sub '----- Public Sub restore() 'restore the state of an object End Sub
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.
'Interface Drawing Public Sub setSelected(b As Boolean) End Sub '----- Public Sub draw(g As PictureBox) End Sub '----- Public Sub move(xpt As Integer, ypt As Integer) End Sub '----- Public Function contains(X As Integer, Y As Integer) As Boolean End Function '----- Public Sub setFill(b as Boolean) End Sub '----- 'Property methods used to save and restore state Property Get rects() As Rectangle End Property '----- Property Set rects(rc As Rectangle) End Property
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.
'Class DrawMemento Implements Memento Private X As Integer, Y As Integer Private w As Integer, h As Integer Private rect As Rectangle Private visDraw As Drawing '----- Private Sub Memento_init(d As Drawing) 'save the state of a visual rectangle Set visDraw = d Set rect = visDraw.rects X = rect.X Y = rect.Y w = rect.w h = rect.h End Sub '----- Private Sub Memento_restore() 'restore the state of a drawing object rect.X = X rect.Y = Y rect.h = h rect.w = w Set visDraw.rects = rect End Sub
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.
'Class DrawInstance Implements Memento 'treats a drawing index as an object Private intg As Integer Private med As Mediator Public Sub init(a As Integer, md As Mediator) intg = a 'remember the index Set med = md End Sub Property Get integ() As Integer integ = intg End Property '----- Private Sub Memento_init(d As Drawing) End Sub '----- Private Sub Memento_restore() 'remove that drawing from the list med.removeDrawing intg End Sub
We handle the FillMemento in just the same way, except that its restore method turns off the fill flag for that drawing element.
'Class FillMemento Implements Memento Private index As Integer Private med As Mediator '----- Public Sub init(a As Integer, md As Mediator) index = a Set med = md End Sub '----- Private Sub Memento_init(d As Drawing) End Sub '----- Private Sub Memento_restore() Dim d As Drawing Set d = med.getDrawing(index) d.setFill False End Sub
VB6 does not have a way to draw filled circles that is analogous to the way we draw filled rectangles. Instead, circles are filled if the Picturebox control's FillStyle is set appropriately. However, in that case, it fills all circles you draw, whether you want to or not. Therefore, for VB6, we approximate filling the circles by drawing concentric circles inside the original circle and then drawing an inscribed filled rectangle as well.
If filled Then For i = r To 1 Step -1 Pic.Circle (xc, yc), i, fillColor Next i Pic.Line (x + 4, y + 4)-(x + w - 6, y + w - 6), fillColor, BF End If
The State pattern in VB7 is similar to that in VB6. We use the same interfaces for the Memento and Drawing classes.
Public Interface Memento Sub restore() End Interface Public Interface Drawing Sub setSelected(ByVal b As Boolean) Sub draw(ByVal g As Graphics) Sub move(ByVal xpt As Integer, ByVal ypt As Integer) Function contains(ByVal x As Integer, _ ByVal y As Integer) As Boolean Sub setFill(ByVal b As Boolean) Property rects() As vbpatterns.Rectangle End Interface
However, there is some advantage in creating a State class with empty methods and overriding only those that a particular derived State class will require. So our base State class is as follows.
Public Class State Public Overridable Sub mouseDown(ByVal x As Integer,_ ByVal y As Integer) End Sub '----- Public Overridable Sub mouseUp(ByVal x As Integer, _ ByVal y As Integer) End Sub '----- Public Overridable Sub mouseDrag(ByVal x As Integer, _ ByVal y As Integer) End Sub '----- Public Overridable Sub selectOne(ByVal d As Drawing) End Sub End Class
Then our derived state classes need only override the methods important to them. For example, the RectState class only responds to MouseDown.
Public Class RectState Inherits State Private med As Mediator Public Sub New(ByVal md As Mediator) med = md End Sub '----- Public Overrides Sub mouseDown(ByVal x As Integer, _ ByVal y As Integer) Dim vr As New VisRectangle(x, y) med.addDrawing(vr) End Sub End Class
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. The revised visRectangle class looks like this.
Public Class VisRectangle Implements Drawing Protected x, y, w, h As Integer Private rect As vbpatterns.Rectangle Protected selected As Boolean Protected filled As Boolean Protected bBrush As SolidBrush Protected rBrush As SolidBrush Protected bPen As Pen Private fillColor As Color '----- Public Sub New(ByVal xp As Integer, _ ByVal yp As Integer) x = xp 'save coordinates y = yp w = 40 'default size h = 30 fillColor = Color.Red bbrush = New SolidBrush(color.Black) rbrush = New SolidBrush(fillcolor) bPen = New Pen(color.Black) saveAsRect() 'keep in rectangle class as well End Sub '----- Protected Sub saveAsRect() rect = New vbpatterns.Rectangle(x, y, w, h) End Sub '----- Public Function contains(ByVal xp As Integer, _ ByVal yp As Integer) As Boolean _ Implements Drawing.contains Return rect.contains(xp, yp) End Function '----- Public Overridable Sub draw(ByVal g As Graphics) _ Implements Drawing.draw 'draw rectangle If filled Then g.FillRectangle(rbrush, x, y, w, h) End If g.DrawRectangle(bpen, x, y, w, h) If selected Then 'draw handles drawHandles(g) End If End Sub '----- Protected Sub drawHandles(ByVal g As Graphics) 'Draws handles on sides of square or circle g.fillrectangle(bBrush, (x + w 2), (y - 2), 4, 4) g.FillRectangle(bbrush, x - 2, y + h 2, 4, 4) g.FillRectangle(bbrush, x + (w 2), y + h - 2, 4, 4) g.FillRectangle(bbrush, x + (w - 2), y + (h 2), 4, 4) End Sub '----- Public Overridable Sub move(ByVal xpt As Integer, _ ByVal ypt As System.Integer) _ Implements VBNetState.Drawing.move 'Moves drawing to new coordinates x = xpt y = ypt saveAsRect() End Sub '----- Friend Property rects() As vbPatterns.Rectangle _ Implements Drawing.rects 'Allows changing of remembered state Set x = value.x y = value.y w = value.w h = value.h saveAsRect() End Set Get Return rect End Get End Property '----- Public Sub setFill(ByVal b As Boolean) _ Implements Drawing.setFill filled = b End Sub '----- Public Sub setSelected(ByVal b As Boolean) _ Implements VBNetState.Drawing.setSelected selected = b End Sub End Class
However, our visCircle class only needs to override the draw method and have a slightly different constructor.
Public Class VisCircle Inherits VisRectangle Private r As Integer '----- Public Sub New(ByVal xp As Integer, _ ByVal yp As Integer) MyBase.New(xp, yp) r = 15 w = 30 h = 30 saveAsRect() End Sub '----- Public Overrides Sub draw(ByVal g As Graphics) 'Fill the circle if flag set If filled Then g.FillEllipse(rbrush, x, y, w, h) End If g.DrawEllipse(bpen, x, y, w, h) If selected Then drawHandles(g) End If End Sub End Class
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. Note that there is a valid fill method in VB7 to fill circles (ellipses).
The Mediator, Memento, and StateManager classes are essentially identical to those we wrote for VB6. However, we can simplify the overall program a great deal by creating derived classes from the ToolBarButton class and making them implement the Command interface as well.
'A toolbar button class that also 'has a command interface Public Class CmdToolbarButton Inherits System.WinForms.ToolBarButton Implements Command Protected med As Mediator Protected selected As Boolean Public Sub New(ByVal caption As String, _ ByVal md As mediator) MyBase.New() Me.Text = caption med = md InitializeComponent() End Sub '----- Public Overridable Sub setSelected(ByRef b As Boolean) selected = b End Sub '----- Public Overridable Sub Execute() _ Implements Command.Execute End Sub End Class
We then can derive our RectButton, CircleButton, ClearButton, Undo Button, FillButton, and PickButton classes from the CmdToolBarButton class and give each of them the appropriate Execute method. The RectButton class is just that straightforward.
Public Class RectButton Inherits CmdToolbarButton '----- Public Sub New(ByVal md As Mediator) MyBase.New("Rectangle", md) Me.Style = ToolBarButtonStyle.ToggleButton med.registerRectButton(Me) End Sub '----- Public Overrides Sub Execute() med.startrectangle() End Sub End Class
The only disadvantage to this approach is that you have to add the buttons to the toolbar programmatically instead of using the designer. However, this just amounts to adding the buttons to a collection. We create the empty toolbar in the designer, giving it the name Tbar, and then add the buttons to it.
Private Sub init() 'called from New constructir '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 Dim sep As New ToolBarButton() sep.Style = ToolBarButtonStyle.Separator TBar.Buttons.Add(sep) TBar.Buttons.Add(undoB) TBar.Buttons.Add(clrb) End Sub
This makes the processing of the button clicks completely object oriented because we do not have to know which button was clicked. They are all Command objects, and we just call their execute methods.
'process button commands Private Sub TBar_ButtonClick( _ ByVal sender As System.Object, _ ByVal e As ToolBarButtonClickEventArgs) _ Handles TBar.ButtonClick Dim cmd As Command Dim tbutn As ToolBarButton = e.Button cmd = CType(tbutn, Command) 'get the command object cmd.Execute() 'and execute it End Sub
One real problem with programs with this many objects 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.
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.
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.
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
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 simplifies and clarifies the program.
In VB, 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.
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.
Rewrite the StateManager to use a Factory pattern to produce the states on demand.
While visual graphics programs provide obvious examples of State patterns, server programs can benefit by this approach. Outline a simple server that uses a state pattern.