Codementor Events

Design Patterns in Rust: Memento or how to undo your actions

Published Jun 03, 2023

The memento pattern can be used to (partially) expose the internal state of an object. One use case for this pattern is to serialize an object to a file or as JSON for example, another is to build an undo stack.

One bit of advice when using this pattern is only expose the state that needs to be exposed, to comply with the Least Privilege Principle.

So, what does it look like? Well, like this:

memento.drawio (1).png

This pattern usually consist of three parts:

  1. The Originator, this is the object whose internal gets exposed.
  2. The Client, this is the object that wants to change the state of the Originator.
  3. Before the state changes, a Memento object is instantiated with the original state, and stored. If you want to build an undo-system, these Memento objects could be pushed onto some stack.

In an empty directory open your terminal or commandline and type:

cargo new memento_pattern
cd memento_pattern

Open main.rs in the src/ directory in your favourite IDE.

We will start by defining the Memento struct:

#[derive(Clone)]
struct Memento<T:Clone> {
    state: T,
}



impl<T:Clone> Memento<T> {
    fn new(initial_state: &T) -> Self {
        Self { state: initial_state.clone() }
    }

    fn get_state(&self) -> &T {
        &self.state
    }

}

In this simplified example the Memento struct has only field, named state. Also note that the Memento struct is a generic struct which only takes types implementing the Clone trait. Why this is will be apparent later.

In the implementation we a simple constructor, and a get_state() method. Why not a set_state() method? Well, in this simple example, whenever the state of the Originator changes, we generate a new Memento struct. This might not be very memory-efficient, and in my situations you could and probably should adopt a different strategy, like re-using Memento struct. However, for the sake of simplicity, I will generate a new Memento struct.

Now let us have a look at the Originator:

struct Originator<T:Clone> {
    state: T,
    memento: Memento<T>,
}

impl<T:Clone> Originator<T> {
    fn new(state: T) -> Self {
        Self {
            state:state.clone(),
            memento: Memento::new(&state)
        }
    }

    fn set_state(&mut self, state: T) {
        self.memento = Memento::new(&self.state);
        self.state = state.clone();
    }

    fn get_state(&self) -> &T {
        &self.state
    }

    fn restore_state(&mut self,mem:&Memento<T>) {
        self.state=mem.get_state().clone();
    }
}

A few notes:

  1. The Originator struct has two fields: its current state, and a Memento struct to hold any previous states.
  2. Note that the Originator is a generic struct which only takes types which implement the Clone trait. This is because we can not always take ownership of the state, and need a clone in such cases.
  3. In the constructor we not only initialize the current state in the Originator, but we also create a new Memento struct with this state, so that we can always go back, even to the initial state.
  4. The set_state() method takes a state of type T, creates a new Memento struct with this state (and since the constructor of Memento clones the state, it does not take ownership of it), and initializes the Originator state variable, also using the clone() method, therefore preventing taking ownership.
  5. The get_state() method simply returns the current state.
  6. The restore_state() gets a reference to a Memento struct. From this, it gets the state, and sets the current state to it. Also, here a the clone() method is used to prevent any ownership issues.

Now we can have a look at the Client struct:

struct Client<T:Clone> {
    originator: Originator<T>
}

impl<T:Clone> Client<T> {
    fn new(originator:Originator<T>)->Self {
        
        Self { originator:originator }
    }

    fn set_state(&mut self,state:T) {
        self.originator.set_state(state);
    }

    fn get_state(&self)->T {
        self.originator.get_state().clone()
    }

    fn restore_state(&mut self) {
        let memento=self.originator.memento.clone();
        self.originator.restore_state(&memento);
    }
 }

A short explanation:

  1. The Client has only one field: an Originator struct.
  2. Just like all the other structs in this example, Client is a generic struct which only takes types implementing the Clone trait.
  3. In the implementation we first find a constructor which takes an Originator struct as its parameter
  4. The set_state() method calls the set_state on the originator to set the state.
  5. The get_state() method gets the state from the Originator and clones it, hence preventing ownership issues.
  6. The restore_state() method first gets the Memento struct from the originator, and clones it. Then it calls the restore_state() method on the originator, to restore the state.

Time for a testdrive:

fn main() {
    let first_state="A".to_string();
    let second_state="B".to_string();
    
    let origin=Originator::new(first_state);
    let mut client=Client::new(origin);

    let old_state=client.get_state();

    client.set_state(second_state.clone());

    let new_state=client.get_state();

    client.restore_state();

    let restored_state=client.get_state();

    println!("Oldstate is {}",old_state);
    println!("Newstate is {}",new_state);
    println!("Restoredstate is {}",restored_state);
}

Line by line:

  1. We create two states, which are defined in first_state and second_state.
  2. Then we create a originator, with the first state, and after that, we initialize a client with this Originator.
  3. Next, we get the old_state.
  4. We set the state of the client to the second_state and get that.
  5. Next we restore the state, and should get a clone of the first_state back
  6. Time to check it all, with print statements.

When I started developing the Rust implementation of this pattern, I thought it would be pretty simple. That is the reason I decided to make it more generic, however, I quickly ran into all kinds of ownership- and borrowing issues. The downside is that it took me a lot of extra time, the upside is that I learned about those two subjects.

The end result you see here is the result of many attempts, but I think it quite an elegant solution. One possible enhancement might be to implement a real undo-stack, but I will do that in a later post.

Discover and read more posts from Iede Snoek
get started
post commentsBe the first to share your opinion
Show more replies