DekGenius.com
[ Team LiB ] Previous Section Next Section

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.

    [ Team LiB ] Previous Section Next Section