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.
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.
The Originator is the object whose state we want to save.
The Memento is another object that saves the state of the Originator.
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.
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.
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.
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.
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 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.
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
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.
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
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.
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.
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.
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.
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.