Design Patterns in Go: Memento or how to undo actions.
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:
This pattern usually consist of three parts:
- The Originator, this is the object whose internal gets exposed.
- The Client, this is the object that wants to change the state of the Originator.
- 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:
go mod init github.com/memento_pattern
Then open your favourite IDE in this directory and add a main.go file.
We will start with the preliminaries, type:
package main
import "fmt"
We will start by defining the Memento struct:
type Memento[T any] struct {
state T
}
func (m *Memento[T]) getState() T {
return m.state
}
func (m *Memento[T]) setState(newState T) {
m.state = newState
}
func createMemento[T any](state T) Memento[T] {
return Memento[T]{
state: state,
}
}
Some explanation is needed:
- The Memento struct is a generic struct, i.e. it must be able to store any kind of type. That is what [T any] denotes.
- The getState() method is self-explanatory, it just returns the state.
- The same goes for the setState() method, it just sets the state.
- The createMemento() is just a utility method to create a Memento object.
The Originator struct is a bit more complex:
type Originator[T any] struct {
state T
memento Memento[T]
}
func (o *Originator[T]) setState(state T) {
o.memento = createMemento(o.state)
o.state = state
}
func (o *Originator[T]) getState() T {
return o.state
}
func (o *Originator[T]) restoreState(mem Memento[T]) {
o.state = mem.state
}
func createOriginator[T any](state T) Originator[T] {
return Originator[T]{
state: state,
memento: createMemento(state),
}
}
The explanation:
- The Originator has two fields: the current state, and a Memento object.
- The getState() method just returns the current state.
- In the setState() method does two things: it makes a snapshot of the current state, and the it sets the new one.
- The restoreState() basically does the reverse: it checks if there is a previous state, and if there is one, it restores it.
- Lastly there is a constructor method, which constructs a new Originator. In it we set the current state of the Originator, and also create a memento for the original state, this will be replaced when we call setState().
Now we can define the Client:
type Client[T any] struct {
originator Originator[T]
}
func (c *Client[T]) setState(state T) {
c.originator.setState(state)
}
func (c *Client[T]) getState() T {
return c.originator.getState()
}
func (c *Client[T]) restoreState() {
c.originator.restoreState(*c.originator.memento)
}
func createClient[T any](state T) Client[T] {
return Client[T]{
originator: createOriginator(state),
}
}
Method by method:
- The Client only has one field in this simplified example: the Originator.
- setState() is passed down to the originator.
- getState() return the current state of the originator.
- restoreState() calls the restoreState() on the originator, and passes the current memento as a parameter. This is done so a different memento object could be passed, like from an undo-stack. This however is not needed in this example.
- createClient() is just a constructor utility function, which returns a new Client.
Now it is time to test:
func main() {
client := createClient("A")
oldState := client.getState()
client.setState("B")
newState := client.getState()
client.restoreState()
restoredState := client.getState()
fmt.Println("Original state: ", oldState)
fmt.Println("New state: ", newState)
fmt.Println("Restored state: ", restoredState)
}
Line by line:
- We create a Client and give it state ‘A’.
- We store the old state in the oldState variable.
- Next we set the state of the Client to ‘B’. That means under the hood a new memento object is created.
- We store the new state in the newState variable.
- Now we restore the old state, and store it in the restoredState variable.
- After all of this we print out the three variables, to check that the old state has been restored, after setting a new state.
When I started writing this I thought this pattern would be pretty easy, but then I started using generics in Go, which I had not done before. That took some extra getting used to, however, the result is quite elegant I think.
One possible extension would be to have some sort of stack. I did not implement this here, for simplicity’s sake.