Memento Pattern

The pattern allows:

  • for capturing and externalizing an object’s internal state so that it can be restored later, all without violating encapsulation.

Memento Pattern


The Memento pattern is implemented with two objects: the originator and a caretaker. The originator is an object that has an internal state. The caretaker performs an operation on the originator, but can undo the change when it needed. The caretaker requests memento object from the originator. Then it applies the operation. The caretaker can roll back the originator to the initial state returning the memento object to the originator.


PHP Example

The example describes a restorable iterator. Client module traversing such iterator can mark it state and rollback after many iterations if it necessary. Imagine you analyzing source code iterating from the array given by token_get_all(). You can only know if you are already on the desired position after checking several sibling elements further. Once confirmed the element sequence is true you can easily move to its beginning.

<?php
/*
 * @category Design Pattern Tutorial
 * @package Memento Sample
 * @author Dmitry Sheiko <me@dsheiko.com>
 * @link http://dsheiko.com
 */
/**
 * Memento
 */
class Memento
{
    private $_originator;
    public function  __construct(&$originator)
    {
        $this->_originator = &$originator;
    }
    public function save()
    {
        $this->_state = $this->_originator->getState();
    }
    public function restore()
    {
        $this->_originator->setState($this->_state);
    }
}
/**
 * Originator
 */
interface Restorable
{
    public function getState();
    public function setState($state);
}
class RestorableIterator implements Iterator, Restorable
{
    private $_data;
    private $_cursor = 0;
    private $_valid = false;

    public function  __construct(array $data = array())
    {
        $this->_data = $data;
        $this->_valid = (bool)count($this->_data);
    }
    public function current()
    {
        return $this->_data[$this->_cursor];
    }
    public function key()
    {
        return $this->_cursor;
    }
    public function getState()
    {
        return $this->_cursor;
    }
    public function setState($state)
    {
        $this->_cursor = $state;
    }
    public function next()
    {
        $max = count($this->_data) - 1;
        $this->_valid = $this->_cursor < $max ? (bool)(++$this->_cursor) : false;
    }
    public function rewind()
    {
        $this->_valid = (bool)count($this->_data);
        $this->_cursor = 0;
    }
    public function valid()
    {
        return $this->_valid;
    }
}
/**
 * Caretaker
 */
$it = new RestorableIterator(array('a', 'b', 'c', 'd'));
$itMemento = new Memento($it);
$it->next();
$itMemento->save();
$it->next();
$it->next();
echo "Current value ", $it->current(), "\n";
$itMemento->restore();
echo "Current value ", $it->current(), "\n";

// Output:
// Current value d
// Current value b

JS/ES5 Example

Here is a classical bookmark implementation. OpenDocument object state keeps reader’s position (page). It can be bookmarked and any of bookmarks can be respectively restored.

/*
 * @category Design Pattern Tutorial
 * @package Memento Sample
 * @author Dmitry Sheiko <me@dsheiko.com>
 * @link http://dsheiko.com
 */
(function() {
/**
 * Memento
 */
var PageBookmarker = function(doc) {
    var _private = {
      bookmarks: []
    };
    return {
        savePage : function(name) {
            if (typeof name !== 'string' || name.length ===  0) {
                throw new TypeError('Insufficient type');
            }
            _private.bookmarks[name] = doc.getPage();
        },
        restorePage : function(name) {
            if (typeof _private.bookmarks[name] === 'undefined') {
                throw new Error('Bookmark not found');
            }
            doc.setPage(_private.bookmarks[name]);
        }
    }
};
/**
 * Originator
 */
var OpenDocument = function() {
    var _private = {
      page: 1
    };
    return {
        setPage: function(page) {
            _private.page = page;
        },
        getPage: function() {
            return _private.page;
        }
    }
};

/**
 * Usage
 */

var doc = new OpenDocument(),
    bookmarker = new PageBookmarker(doc);

doc.setPage(2);
bookmarker.savePage('test');
doc.setPage(4);
console.log('Current page ' + doc.getPage());
bookmarker.restorePage('test');
console.log('Saved page ' + doc.getPage());

// Output:
// Current page 4
// Saved page 2

}());