Chapter 27. The Memento Pattern

In this chapter, we discuss how to use the Memento pattern to save data about an object so you can restore it later. For example, you might like to save the color, size, pattern, or shape of objects in a drafting or painting program. Ideally, it should be possible to save and restore this state without making each object take care of this task and without violating encapsulation. This is the purpose of the Memento pattern.

Motivation

Objects normally shouldn't expose much of their internal state using public methods, but you would still like to be able to save the entire state of an object because you might need to restore it later. In some cases, you could obtain enough information from the public interfaces (such as the drawing position of graphical objects) to save and restore that data. In other cases, the color, shading, angle, and connection relationships to other graphical objects need to be saved, and this information is not readily available. This sort of information saving and restoration is common in systems that need to support Undo commands.

If all of the information describing an object is available in public variables, it is not that difficult to save them in some external store. However, making these data public makes the entire system vulnerable to change by external program code, when we usually expect data inside an object to be private and encapsulated from the outside world.

The Memento pattern attempts to solve this problem in some languages by having privileged access to the state of the object you want to save. Other objects have only a more restricted access to the object, thus preserving their encapsulation. In VB6, however, there is no such thing as privileged access and we will see this is true only to a limited degree in VB7.

This pattern defines three roles for objects.

  1. The Originator is the object whose state we want to save.

  2. The Memento is another object that saves the state of the Originator.

  3. The Caretaker manages the timing of the saving of the state, saves the Memento, and, if needed, uses the Memento to restore the state of the Originator.

Implementation

Saving the state of an object without making all of its variables publicly available is tricky and can be done with varying degrees of success in various languages. Design Patterns suggests using the C++ friend construction to achieve this access, and the Smalltalk Companion notes that it is not directly possible in Smalltalk. In Java, this privileged access is possible using the package protected mode. In VB6, like Smalltalk, this is not directly possible. The friend keyword is available in both VB6 and VB7, but all that means is any class method labeled as friend will only be accessible within the project. If you make a library from such classes, the methods marked as friends will not be exported and available. Instead, we will define a property to fetch and store the important internal values and make use of no other properties for any purpose in that class. For consistency, we'll use the friend keyword on these properties, but remember that this linguistic use of friend is not very restrictive.

Sample Code

Let's consider a simple prototype of a graphics drawing program that creates rectangles and allows you to select them and move them around by dragging them with the mouse. This program has a toolbar containing three buttons—Rectangle, Undo, and Clear—as we see in Figure 27-1.

A simple graphics drawing program that allows you to draw rectangles, undo their drawing, and clear the screen

Figure 27-1. A simple graphics drawing program that allows you to draw rectangles, undo their drawing, and clear the screen

The Rectangle button is a toolbar ToggleButton that stays selected until you click the mouse to draw a new rectangle. Once you have drawn the rectangle, you can click in any rectangle to select it, as we see in Figure 27-2.

Selecting a rectangle causes “handles” to appear, indicating that it is selected and can be moved.

Figure 27-2. Selecting a rectangle causes “handles” to appear, indicating that it is selected and can be moved.

Once it is selected, you can drag that rectangle to a new position, using the mouse, as shown in Figure 27-3.

The same selected rectangle after dragging

Figure 27-3. The same selected rectangle after dragging

The Undo button can undo a succession of operations. Specifically, it can undo moving a rectangle, and it can undo the creation of each rectangle. There are five actions we need to respond to in this program.

  1. Rectangle button click

  2. Undo button click

  3. Clear button click

  4. Mouse click

  5. Mouse drag

The three buttons can be constructed as Command objects, and the mouse click and drag can be treated as commands as well. Since we have a number of visual objects that control the display of screen objects, this suggests an opportunity to use the Mediator pattern, and that is, in fact, the way this program is constructed.

We will create a Caretaker class to manage the Undo action list. It can keep a list of the last n operations so they can be undone. The Mediator maintains the list of drawing objects and communicates with the Caretaker object as well. In fact, since there could be any number of actions to save and undo in such a program, a Mediator is virtually required so there is a single place to send these commands to the Undo list in the Caretaker.

In this program, we save and undo only two actions: creating new rectangles and changing the position of rectangles. Let's start with our visRectangle class, which actually draws each instance of the rectangles.

'Class VisRectangle
Dim x As Integer, y As Integer, w As Integer, h As Integer
Private rect As Rectangle
Private selected As Boolean
'-----
Public Sub init(xp As Integer, yp As Integer)
 x = xp     'save coordinates
 y = yp
 w = 40     'default size
 h = 30
 saveAsRect 'keep in rectangle class as well
End Sub
'-----
'Property methods used to save and restore state
Friend Property Get rects() As Rectangle
 Set rects = rect
End Property
'-----
Friend Property Set rects(rc As Rectangle)
 x = rc.x
 y = rc.y
 w = rc.w
 h = rc.h
 saveAsRect
End Property
'-----
Public Sub setSelected(b As Boolean)
 selected = b
End Sub
'-----
'save values in Rectangle class
Private Sub saveAsRect()
 Set rect = New Rectangle
 rect.init x, y, w, h
End Sub
'-----
'draw rectangle and handles
Public Sub draw(Pic As PictureBox)
'draw rectangle
 Pic.Line (x, y)-(x + w, y + h), , B
 If selected Then   'draw handles
  Pic.Line (x + w / 2, y - 2)- _
    (x + w / 2 + 4, y + 2), , BF
  Pic.Line (x - 2, y + h / 2)- _
    (x + 2, y + h / 2 + 4), , BF
  Pic.Line (x + (w / 2), y + h - 2)- _
    (x + (w / 2) + 4, y + h + 2), , BF
  Pic.Line (x + (w - 2), y + (h / 2))- _
    (x + (w + 2), y + (h / 2) + 4), , BF
 End If
End Sub
'-----
Public Function contains(xp As Integer, yp As Integer) As Boolean
 contains = rect.contains(xp, yp)
End Function
'-----
Public Sub move(xpt As Integer, ypt As Integer)
 x = xpt
 y = ypt
 saveAsRect
End Sub

We also use a Rectangle class that contains Get and Let properties for the x, y, w, and h values and a contains method.

Drawing the rectangle is pretty straightforward. Now, let's look at our simple Memento class that we use to store the state of a rectangle.

'Class Memento
Private x As Integer, y As Integer
Private w As Integer, h As Integer
Private rect As Rectangle
Private visRect As VisRectangle
'-----
Public Sub init(vrect As VisRectangle)
'save the state of a visual rectangle
 Set visRect = vrect
 Set rect = vrect.rects
 x = rect.x
 y = rect.y
 w = rect.w
 h = rect.h
End Sub
'-----
Public Sub restore()
'restore the state of a visual rectangle
 rect.x = x
 rect.y = y
 rect.h = h
 rect.w = w
 Set visRect.rects = rect
End Sub

When we create an instance of the Memento class, we pass it the visRectangle instance we want to save, using the init method. It copies the size and position parameters and saves a copy of the instance of the visRectangle itself. Later, when we want to restore these parameters, the Memento knows which instance to which it must restore them, and it can do it directly, as we see in the restore() method.

The rest of the activity takes place in the Mediator class, where we save the previous state of the list of drawings as an integer on the undo list.

Public Sub createRect(ByVal x As Integer, ByVal y As Integer)
Dim count As Integer
Dim v As VisRectangle
 unpick             'make sure no rectangle is selected
 If startRect Then  'if rect button is depressed
     count = drawings.count
     caretakr.add count 'Save previous drawing list size
     Set v = New VisRectangle   'create a rectangle
     v.init x, y
     drawings.add v             'add new element to list
     startRect = False          'done with this rectangle
     rect.setSelected False     'unclick button
     canvas.Refresh
   Else
     pickRect x, y  'if not pressed look for rect to select
   End If
End Sub

On the other hand, if you click on the panel when the Rectangle button has not been selected, you are trying to select an existing rectangle. This is tested here.

Public Sub pickRect(x As Integer, y As Integer)
'save current selected rectangle
'to avoid double save of undo
  Dim lastPick As Integer
  Dim v As VisRectangle
  Dim i As Integer
  If selectedIndex > 0 Then
    lastPick = selectedIndex
    End If
    unpick  'undo any selection
    'see if one is being selected
    For i = 1 To drawings.count
       Set v = drawings(i)
       If v.contains(x, y) Then 'did click inside a rectangle
          selectedIndex = i     'save it
          rectSelected = True
          If selectedIndex <> lastPick Then 'but not twice
            caretakr.rememberPosition drawings(selectedIndex)
          End If
          v.setSelected True   'turn on handles
          repaint              'and redraw
       End If
    Next i
End Sub

The Caretaker class remembers the previous position of the rectangle in a Memento object and adds it to the undo list.

Public Sub rememberPosition(vrect As VisRectangle)
   Dim m As Memento
   Set m = New Memento
   m.init vrect
   undoList.add m
End Sub

The Caretaker class manages the undo list. This list is a Collection of integers and Memento objects. If the value is an integer, it represents the number of drawings to be drawn at that instant. If it is a Memento, it represents the previous state of a visRectangle that is to be restored. In other words, the undo list can undo the adding of new rectangles and the movement of existing rectangles.

Our undo method simply decides whether to reduce the drawing list by one or to invoke the restore method of a Memento.

Public Sub undo()
Dim obj As Object
 If undoList.count > 0 Then
  'get last element in undo list
    Set obj = undoList(undoList.count)
    undoList.remove undoList.count   'and remove it
    If Not (TypeOf obj Is Memento) Then
         removeLast        'remove Integer
    Else
         remove obj        'remove Memento
    End If
  End If
 End Sub

This Undo method requires that all the elements in the Collection be objects rather than a mixture of integers and Memento objects. So we create a small wrapper class to convert the integer count into an object.

'Class intClass
'treats an integer as an object
Private intg As Integer
Public Sub init(a As Integer)
 intg = a
End Sub
'-----
Property Get integ() As Integer
 integ = intg
End Property

Instances of this class are created when we add an integer to the undo list.

Public Sub add(intObj As Integer)
        Dim integ As intClass
        Set integ = New intClass
        integ.init intObj
        undoList.add integ
    End Sub

The two remove methods either reduce the number of drawings or restore the position of a rectangle.

Private Sub removeLast()
drawings.remove drawings.count
End Sub
'-----
Private Sub remove(obj As Memento)
 obj.restore
End Sub

A Cautionary Note

While it is helpful in this example to detect the differences between a Memento of a rectangle position and an integer specifying the addition of a new drawing, this is in general an absolutely terrible example of OO programming. You should never need to check the type of an object to decide what to do with it. Instead, you should be able to call the correct method on that object and have it do the right thing.

A more correct way to have written this example would be to have both the intClass and what we are calling the Memento class both have their own restore methods and have them both be members of a general Memento class (or interface). We take this approach in the State example pattern in the next chapter.

Command Objects in the User Interface

We can also use the Command pattern to help in simplifying the code in the user interface. The three buttons are toolbar buttons that are of the class MSComctlLib.Button. We create parallel command object classes for each of the buttons and have them carry out the actions in conjunction with the mediator.

Private Sub Form_Load()
 Set med = New Mediator     'create the mediator
 med.init
 med.registerCanvas Pic
 Set rectB = New RectButton     'rectangle button
 rectB.init med, tbar.Buttons(1)
 Set ubutn = New UndoButton     'undo button
 ubutn.init med, tbar.Buttons(2)
 Set clrb = New ClearButton     'clear button
 clrb.init med
 Set commands = New Collection  'make a list of them
 commands.add rectB
 commands.add ubutn
 commands.add clrb
End Sub

Then the command interpretation devolves to just a few lines of code, since all the buttons call the same click event already.

Private Sub tbar_ButtonClick(ByVal Button As
MSComctlLib.Button)
 Dim i As Integer
 Dim cmd As Command
 i = Button.Index       'get which button
 Set cmd = commands(i)  'get that command
 cmd.Execute            'execute it
End Sub

The RectButton command class is where most of the activity takes place.

'Class RectButton
Implements Command
Private bt As MSComctlLib.Button
Private med As Mediator
'-----
Public Sub init(md As Mediator, but As MSComctlLib.Button)
 Set bt = but
 Set med = md
 med.registerRectButton Me
End Sub
'-----
Private Sub Command_Execute()
 If bt.Value = tbrPressed Then
   med.startRectangle
 End If
End Sub
'-----
Public Sub setSelected(sel As Boolean)
 If sel Then
   bt.Value = tbrPressed
 Else
   bt.Value = tbrUnpressed
 End If
End Sub

Handling Mouse and Paint Events

We also must catch the mouse down, up, and move events and pass them on to the Mediator to handle.

Private Sub Pic_MouseDown(Button As Integer, _
       Shift As Integer, x As Single, y As Single)
 mouse_down = True
 med.createRect x, y
End Sub
Private Sub Pic_MouseMove(Button As Integer, _
        Shift As Integer, x As Single, y As Single)
 If mouse_down Then
   med.drag x, y
 End If
End Sub
Private Sub Pic_MouseUp(Button As Integer, Shift As Integer, _
    x As Single, y As Single)
 mouse_down = False
End Sub

Whenever the Mediator makes a change, it calls for a refresh of the picture box, which in turn calls the Paint event. We then pass this back to the Mediator to draw the rectangles in their new positions.

Private Sub Pic_Paint()
 med.reDraw Pic
End Sub

The complete class structure is diagrammed in Figure 27-4.

The UML diagram for the drawing program using a Memento

Figure 27-4. The UML diagram for the drawing program using a Memento

Writing a Memento in VB.NET

We can write almost the same code in VB7. We will use the same friend keyword to indicate the somewhat restricted nature of the properties and save and restore rectangle state. For our visRectangle class, we can declare the rect property as having a friend modifier.

'Property methods used to save and restore state
    Friend Property rects() As vbpatterns.Rectangle
        Set
            x = value.x
            y = value.y
            w = value.w
            h = value.h
            saveAsRect()
        End Set
        Get
            Return rect
        End Get
    End Property

As in VB6, this approach is almost the same as having public access to the method, with the exception that if you compile the code into a library, these methods are not visible. So while this friend property is much less restrictive than the C++ friend modifier, it is still slightly restrictive.

The remainder of the program can be written in much the same way as for VB6. The visRectangle class's draw method is only slightly different, since it uses the Graphics object,

'draw rectangle and handles
Public Sub draw(ByVal g As Graphics)
'draw rectangle
 g.DrawRectangle(bpen, x, y, w, h)

 If selected Then   'draw handles
  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 If
End Sub

However, the Memento saves and restores a Rectangle object in much the same way.

You can build a toolbar and create ToolbarButtons in VB7 using the IDE, but if you do, it is difficult to subclass them to make them into command objects. There are two possible solutions. First, you can keep a parallel array of Command objects for the RectButton, the UndoButton, and the Clear button and call them in the toolbar click routine.

You should note, however, that the toolbar buttons do not have an Index property, and you cannot just ask which one has been clicked by its index and relate it to the command array. Instead, we can use the getHashCode property of each tool button to get a unique identifier for that button and keep the corresponding command objects in a Hashtable keyed off these button hash codes. We construct the Hashtable as follows.

 Private Sub init()
        'called from New constructor
        med = New Mediator(Pic)     'create Mediator
        commands = New Hashtable()  'and Hash table
        'create the command objects
        Dim rbutn As New RectButton(med, Tbar.Buttons(0))
        Dim ubutn As New UndoButton(med, Tbar.Buttons(1))
        Dim clrbutn As New Clearbutton(med)
        'add them to the hashtable using the button hash values
        commands.Add(btRect.GetHashCode, rbutn)
        commands.Add(btundo.GetHashCode, ubutn)
        commands.Add(btClear.GetHashCode, clrbutn)
        AddHandler Pic.Paint, _
            New PaintEventHandler(AddressOf paintHandler)
    End Sub

We can use these hash codes to get the right command object when the buttons are clicked.

Protected Sub tBar_ButtonClick(ByVal sender As Object, _
            ByVal e As ToolBarButtonClickEventArgs)
        Dim cmd As Command
        Dim tbutn As ToolBarButton = e.button
        cmd = CType(commands(tbutn.GetHashCode), Command)
        cmd.Execute()
    End Sub

The VB7 version is shown in Figure 27-5.

The VB.Net version of Memento

Figure 27-5. The VB.Net version of Memento

Alternatively, you could create the toolbar under IDE control but add the tool buttons to the collection programmatically and use derived buttons with a Command interface instead. We illustrate this approach in the State pattern.

Consequences of the Memento

The Memento provides a way to preserve the state of an object while preserving encapsulation in languages where this is possible. Thus, data to which only the Originator class should have access effectively remain private. It also preserves the simplicity of the Originator class by delegating the saving and restoring of information to the Memento class.

On the other hand, the amount of information that a Memento has to save might be quite large, thus taking up fair amounts of storage. This further has an effect on the Caretaker class that may have to design strategies to limit the number of objects for which it saves state. In our simple example, we impose no such limits. In cases where objects change in a predictable manner, each Memento may be able to get by with saving only incremental changes of an object's state.

In our example code in this chapter, we have to use not only the Memento but the Command and Mediator patterns as well. This clustering of several patterns is very common, and the more you see of good OO programs, the more you will see these pattern groupings.

Thought Question

Thought Question

Mementos can also be used to restore the state of an object when a process fails. If a database update fails because of a dropped network connection, you should be able to restore the data in your cached data to their previous state. Rewrite the Database class in the Façade chapter to allow for such failures.

Programs on the CD-ROM

Memento VB6 Memento
MementoVBNet VB7 Memento
..................Content has been hidden....................

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