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.
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);} } }
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(" ROLLBACK(" + 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.