Recipe 3.35 Rolling Back Object Changes
Problem
You have an object that allows
its state to be changed. However, you do not want these changes to
become permanent if other changes to the system cannot be made at the
same time. In other words, you want to be able to roll back the
changes if any of a group of related changes fails.
Solution
Use the memento design
pattern to allow your object to save its original state in
order to roll back changes. The SomeDataOriginator
class defined for this recipe contains data that must be changed only
if other system changes occur. Its source code
is:
using System;
using System.Collections;
public class SomeDataOriginator
{
public SomeDataOriginator( ) {}
public SomeDataOriginator(int state, string id, string clsName)
{
this.state = state;
this.id = id;
this.clsName = clsName;
}
private int state = 1;
private string id = "ID1001";
private string clsName = "SomeDataOriginator";
public string ClassName
{
get {return (clsName);}
set {clsName = value;}
}
public string ID
{
get {return (id);}
set {id = value;}
}
public void ChangeState(int newState)
{
state = newState;
}
public void Display( )
{
Console.WriteLine("State: " + state);
Console.WriteLine("Id: " + id);
Console.WriteLine("clsName: " + clsName);
}
// Nested Memento class used to save outer class's
state internal class Memento
{
internal Memento(SomeDataOriginator data)
{
this.state = data.State;
this.id = data.id;
this.clsName = data.clsName;
originator = data;
}
private SomeDataOriginator originator = null;
private int state = 1;
private string id = "ID1001";
private string clsName = "SomeDataOriginator";
internal void Rollback( )
{
originator.clsName = this.clsName;
originator.id = this.id;
originator.state = this.state;
}
}
}
The MementoCareTaker is the caretaker object,
which saves a single state that the originator object can roll back
to. Its source code is:
public class MementoCareTaker
{
private SomeDataOriginator.Memento savedState = null;
internal SomeDataOriginator.Memento Memento
{
get {return (savedState);}
set {savedState = value;}
}
}
MultiMementoCareTaker is another caretaker object
that can save multiple states to which the originator object can roll
back. Its source code is:
public class MultiMementoCareTaker
{
private ArrayList savedState = new ArrayList( );
internal SomeDataOriginator.Memento this[int index]
{
get {return ((SomeDataOriginator.Memento)savedState[index]);}
set {savedState[index] = (SomeDataOriginator.Memento)value;}
}
internal void Add(SomeDataOriginator.Memento memento)
{
SavedState.Add(memento);
}
internal int Count
{
get {return (savedState.Count);}
}
}
Discussion
The memento design pattern allows object state
to be saved so that it can be restored in response to a specific
situation. The memento pattern is very useful for implementing
undo/redo or commit/rollback actions. This pattern usually has an
originator object—a new or existing object
that needs to have an undo/redo or commit/rollback style behavior
associated with it. This originator object's
state—the values of its fields—will be mirrored in a
memento object, which is an object that can
store the state of an originator object. Another object that usually
exists in this type of pattern is the caretaker
object. The caretaker is responsible for saving one or
more memento objects, which can then be used later to restore the
state of an originator object. This recipe makes use of two caretaker
objects. The first, MementoCareTaker, saves a
single object state that can later be used to roll an object back.
The second, MultiMementoCareTaker, uses an
ArrayList object to save multiple object states,
thereby allowing many levels of rollbacks to occur. You can also
think of MultiMementoCareTaker as storing multiple
levels of undo/redo state.
The originator class, SomeDataOriginator, has the
state, id, and
clsName fields to store information. The
originator class has a nested Memento class that
needs to access the fields of the originator directly.
One thing we have to add to the class, that will not affect how it
behaves or how it is used, is a nested Memento
class. This nested class is used to store the state of its outer
class. We use a nested class so that it can access the private fields
of the outer class. This allows the Memento object
to get copies of all the needed fields of the originator object
without having to add special logic to the originator to allow it to
give this field information to the Memento object.
The Memento class contains only private fields
that mirror the fields in the outer object that you want to store.
Note that you do not have to store all fields of an outer type, just
the ones that you want to roll back or undo. The
Memento object also contains a constructor that
accepts a SomeDataOriginator object. The
constructor saves the pointer to this object as well as its current
state. There is also a single method called
Rollback. The Rollback method
is central to restoring the state of the current
SomeDataOriginator object. This method uses the
originator pointer to this object to set the
SomeDataOriginator object's
fields back to the values contained in this instance of the
Memento object.
The caretaker objects store any Memento objects
created by the application. The application can then specify which
Memento objects to use to roll back an
object's state. Remember that each
Memento object knows which originator object to
roll back. Therefore, you need to tell the caretaker object only to
use a Memento object to roll back an object, and
the Memento object takes care of the rest.
There is a potential problem with the caretaker objects that is
easily remedied. The problem is that the caretaker objects are not
supposed to know anything about the Memento
objects. The caretaker objects in this recipe see only one method,
the Rollback method, that is specific to the
Memento objects. So, for this recipe, this is not
really a problem. However, if you decide to add more logic to the
Memento class, you need a way to shield it from
the caretaker. You do not want another developer to add code to the
caretaker objects that may allow it to change the internal state of
any Memento objects they contain.
To the caretaker objects, each Memento object
should simply be an object that contains the
Rollback method. To make the
Memento objects appear this way to the caretaker
objects, we can place an interface on the Memento
class. This interface is defined as follows:
public interface IMemento
{
void Rollback( );
}
The Memento class is then modified as follows
(changes are highlighted):
internal class Memento : IMemento
{
public void Rollback( )
{
originator.clsName = this.clsName;
originator.id = this.id;
originator.state = this.state;
}
// The rest of this class does not change
}
The caretaker classes are modified as follows (changes are
highlighted):
internal class MementoCareTaker
{
private IMemento savedState = null;
internal IMemento Memento
{
get {return (savedState);}
set {savedState = value;}
}
}
internal class MultiMementoCareTaker
{
private ArrayList savedState = new ArrayList( );
internal IMemento this[int index]
{
get {return ((SomeDataOriginator.Memento)savedState[index]);}
set {savedState[index] = (SomeDataOriginator.Memento)value;}
}
internal void Add(IMemento memento)
{
savedState.Add(memento);
}
internal int Count
{
get {return (savedState.Count);}
}
}
Implementing the IMemento interface serves two
purposes. First, it prevents the caretaker classes from knowing
anything about the internals of the Memento
objects they contain. Second, it allows the caretaker objects to
handle any type of Memento object, so long as it
implements the IMemento interface.
The following code shows how the
SomeDataOriginator, Memento,
and caretaker objects are used. It uses the
MementoCareTaker object to store a single state of
the SomeDataOriginator object and then rolls the
changes back after the SomeDataOriginator object
is modified:
// Create an originator and default its internal state
SomeDataOriginator data = new SomeDataOriginator( );
Console.WriteLine("ORIGINAL");
data.Display( );
// Create a caretaker object
MementoCareTaker objState = new MementoCareTaker( );
// Add a memento of the original originator object to the caretaker
objState.Memento = new SomeDataOriginator.Memento(data);
// Change the originator's internal state
data.ChangeState(67);
data.ID = "foo";
data.ClassName = "bar";
Console.WriteLine("NEW");
data.Display( );
// Rollback the changes of the originator to its original state
objState.Memento.Rollback( );
Console.WriteLine("ROLLEDBACK");
data.Display( );
The use of the MultiMementoCareTaker object is
very similar to the MementoCareTaker object, as
the following code shows:
SomeDataOriginator data = new SomeDataOriginator( );
Console.WriteLine("ORIGINAL");
data.Display( );
MultiMementoCareTaker multiObjState = new MultiMementoCareTaker( );
multiObjState.Add(new SomeDataOriginator.Memento(data));
data.ChangeState(67);
data.ID = "foo";
data.ClassName = "bar";
Console.WriteLine("NEW");
data.Display( );
multiObjState.Add(new SomeDataOriginator.Memento(data));
data.ChangeState(671);
data.ID = "foo1";
data.ClassName = "bar1";
Console.WriteLine("NEW1");
data.Display( );
multiObjState.Add(new SomeDataOriginator.Memento(data));
data.ChangeState(672);
data.ID = "foo2";
data.ClassName = "bar2";
Console.WriteLine("NEW2");
data.Display( );
multiObjState.Add(new SomeDataOriginator.Memento(data));
data.ChangeState(673);
data.ID = "foo3";
data.ClassName = "bar3";
Console.WriteLine("NEW3");
data.Display( );
for (int Index = (multiObjState.Count - 1); Index >= 0; Index--)
{
Console.WriteLine("\r\nROLLBACK(" + Index + ")");
multiObjState[Index].Rollback( );
data.Display( );
}
This code creates a SomeDataOriginator object and
changes its state several times. At every state change, a new
Memento object is created to save the
SomeDataOriginator object's state
at that point in time. At the end of this code, a
for loop iterates over each
Memento object stored in the
MultiMementoCareTaker object, from the most recent
to the earliest. On each iteration of this loop, the
Memento object is used to restore the state of the
SomeDataOriginator object.
|