Design Patterns in Java: Memento
- A Classic Example: Using Memento for Undo
- Memento Durability
- Persisting Mementos Across Sessions
- Summary
Sometimes, the object you want to create is one that existed previously. This occurs when you want to let a user undo operations, revert to an earlier version of work, or resume previously suspended work.
The intent of the Memento pattern is to provide storage and restoration of an object’s state.
A Classic Example: Using Memento for Undo
Chapter 17, Abstract Factory, introduced a visualization application that lets users perform operational modeling experiments with material flow through a factory. Suppose that the functionality for the Undo button has not yet been implemented. We can apply the Memento pattern to make the Undo button work.
A memento is an object that holds state information. In the visualization application, the state we need to preserve is that of the application. Whenever adding or moving a machine, a user should be able to undo that change by clicking Undo. To add the undo function to the visualization, we will need to decide how to capture the state of the application in a memento. We will also have to decide when to capture this state, and how to restore the application to a previously captured state. When the application starts up, it appears as in Figure 19.1.
Figure 19.1 When the visualization application starts up, the work panel is blank, and the Undo button is disabled.
The visualization starts up in an empty state, which is a state. Any time that the visualization is in this empty state, the Undo button should be disabled. After a few adds and drags, the visualization might appear as in Figure 19.2.
Figure 19.2 Users can add and rearrange machines in the visualization.
The state that we need to capture in a memento consists of a list of the locations of the machines that the user has placed. We can keep a stack of these mementos, popping one each time the user clicks Undo.
- Each time the user adds or moves a machine in the visualization, your code will create a memento of the simulated factory and add it to a stack.
- Each time the user clicks the Undo button, your code will pop the most recent memento and then restore the simulation to the state stored at the top of the stack.
When your visualization starts up, you will stack an initial, empty memento and never pop it, to ensure that the top of the stack is always a valid memento. Any time the stack contains only one memento, you disable the Undo button.
We might write the code for this program in a single class, but we expect to add features that support operational modeling and other features that your users may request. Eventually, the application may grow large, so it is wise to use an MVC design that you can build on. Figure 19.3 shows a design that moves the work of modeling the factory into a separate class.
Figure 19.3 This design divides the application’s work into separate classes for modeling the factory, providing GUI elements, and handling a user’s clicks.
This design lets you first focus on developing a FactoryModel class that has no GUI controls and no dependency on the GUI.
The FactoryModel class is at the center of our design. It is responsible for maintaining the current configuration of machines and for maintaining mementos of previous configurations.
Each time a client asks the factory to add or move a machine, the factory will create a copy—a memento—of its current locations and push this onto its stack of mementos. In this example, we do not need a special Memento class. Each memento is merely a list of points: the list of machine locations at a particular time.
The factory model must provide events that let clients register interest in changes to the factory state. This lets the visualization GUI inform the model of changes that the user makes. Suppose that you want the factory to let clients register for the events of adding a machine and dragging a machine. Figure 19.4 shows a design for a FactoryModel class.
Figure 19.4 The FactoryModel class maintains a stack of factory configurations and lets clients register for notification of changes in the factory.
The design in Figure 19.4 calls for the FactoryModel class to provide clients with the ability to register for notification of several events. For example, consider the event of adding a machine. Any registered ChangeListener object will be notified of this change:
package com.oozinoz.visualization; // ... public class FactoryModel { private Stack mementos;
private ArrayList listeners = new ArrayList(); public FactoryModel() { mementos = new Stack(); mementos.push(new ArrayList()); } //... }
The constructor starts off the factory’s initial configuration as an empty list. The remaining methods in the class maintain the stack of machine configuration mementos and fire events that correspond to any changes. For example, to add a machine to the current configuration, a client can call the following method:
public void add(Point location) { List oldLocs = (List) mementos.peek(); List newLocs = new ArrayList(oldLocs); newLocs.add(0, location); mementos.push(newLocs); notifyListeners(); }
This code creates a new list of machine locations and pushes the list on the stack of mementos that the factory model maintains. A subtlety here is that the code ensures that the new machine is first in this list. This is a clue to the visualization that a picture of this machine should appear in front of any other machines that the picture may overlap.
A client that registers for the change notification might update its view of the factory model by rebuilding itself entirely on receiving any event from the factory model. The factory model’s latest configuration is always available from getLocations(), whose code is as follows:
public List getLocations() { return (List) mementos.peek(); }
The undo() method of the FactoryModel class lets a client change the model of machine locations to be a previous version. When this code executes, it also calls notifyListeners().
Challenge 19.1
Write the code for the FactoryModel class’s undo() method.
A solution appears on page 400.
An interested client can provide undo support by registering as a listener, supplying a method that rebuilds the client’s view of the factory. The Visualization class is one such client.
The MVC design in Figure 19.3 separates the tasks of translating user actions from the tasks of maintaining the GUI. The Visualization class creates its GUI controls but passes off the handling of GUI events to a mediator. The VisMediator class translates GUI events into appropriate changes in the factory model. When the model changes, the GUI may need to update. The Visualization class registers for the notification that the FactoryModel class provides. Note the division of responsibility.
- The visualization changes factory events into GUI changes.
- The mediator translates GUI events into factory changes.
Figure 19.5 shows the three collaborating classes in more detail.
Figure 19.5 The mediator translates GUI events into factory model changes, and the visualization reacts to factory events to update the GUI.
Suppose that while dragging a machine image, a user accidentally drops it in the wrong spot and clicks Undo. To be able to handle this click, the visualization registers the mediator for notification of button events. The code for the Undo button in the Visualization class is:
protected JButton undoButton() { if (undoButton == null) { undoButton = ui.createButtonCancel(); undoButton.setText("Undo"); undoButton.setEnabled(false); undoButton.addActionListener(mediator.undoAction()); }
return undoButton; }
This code passes responsibility for handling a click to the mediator. The mediator informs the factory model of any requested changes. The mediator translates an undo request to a factory change with the following code:
private void undo(ActionEvent e) { factoryModel.undo(); }
The factoryModel variable in this method is an instance of FactoryModel that the Visualization class creates and passes the mediator in the VisMediator class’s constructor. We have already examined the FactoryModel class’s pop() command. The flow of messages that occurs when the user clicks Undo appears in Figure 19.6.
Figure 19.6 This diagram shows the message flow that occurs when the user clicks Add.
When the FactoryModel class pops its current configuration, exposing the previous configuration it stored as a memento, the undo() method notifies any ChangeListeners. The Visualization class registers for this in its constructor:
public Visualization(UI ui) { super(new BorderLayout()); this.ui = ui; mediator = new VisMediator(factoryModel); factoryModel.addChangeListener(this); add(machinePanel(), BorderLayout.CENTER); add(buttonPanel(), BorderLayout.SOUTH); }
For each machine location in the factory model, the visualization maintains a Component object that it creates with its createPictureBox() method. The stateChanged() method must clear all the picture box controls from the machine panel, and re-add new picture boxes from the current set of locations in the factory model. The stateChanged() method must also disable the Undo button if the factory has only a single memento left on its stack.
Challenge 19.2
Write the stateChanged() method for the Visualization class.
A solution appears on page 401.
The Memento pattern lets you save and restore an object’s state. A common use of Memento is related to providing undo support in applications. In some applications, as in the factory visualization example, the repository for the information you need to store can be another object. In other cases, you may need to store mementos in a more durable form.