Wednesday, March 03, 2010

Practical Php Patterns: Memento

This post is part of the Practical Php Pattern series.

The pattern of today is the Memento one, whose intent is storing the state of an Originator object without breaking its encapsulation, which typically consists in a set of private fields.
For the sake of preserving encapsulation, the ConcreteMemento should be created only by its correspondent Originator. Only Originator has access to its private members, and only Originator can restore its state given that a ConcreteMemento has been handed back to it.
Another caveat is that only Originator sees the full contract of ConcreteMemento, while the Caretaker objects, which pass the Memento around, work with a very narrow interface, mostly present only for type safety and possibly without any methods on it. Any access to the ConcreteMemento data out of the Originator will, again, break its encapsulation and the best way to avoid tampering is defining Caretaker's method signatures to accept a Memento instead of ConcreteMemento: for instance, you can unit test them with a Memento mock.
ConcreteMemento itself is a dumb object, usually implemented as an immutable ValueObject, with little or no behavior. Thus, there is no need for a factory that takes care of its creation and it can be instantiated directly both in Originator and in test suites (no behavior to stub out).
Php's dynamic typing is very useful since avoids the casting of Memento to ConcreteMemento when is passed back to the Originator. What in a statically typed language should be seriously dealt with, in php is a minor inconvenience.
Anyway, why not storing the whole object whose state we want to preserve instead of a dumb, little Memento? I see two diffused situations where this pattern is applicable:
  • the object whose state has to be maintained is heavy, and serializing it would bring together many other collaborators.
  • moreover, in php scripts it may be linked to external services or resources that do not survive the http request.
The code sample approaches the latter case, tackling the problem by recreating a Service and re-establishing its state with a serializable Memento. The Service may contain anything from database connections to stream wrappers - only the Memento should be stored in $_SESSION.
<?php
/**
 * This interface is primarily here for type safety,
 * specifically to avoid that someone passes to Caretaker's
 * methods strings, instead of Memento instances.
 */
interface Memento
{
}

/**
 * The ConcreteMemento should be used only in the Originator
 * (and in testing).
 */
class ConcreteMemento implements Memento
{
    protected $_url;
    protected $_currentLine;

    public function __construct($url, $currentLine)
    {
        $this->_url = $url;
        $this->_currentLine = $currentLine;
    }

    public function getUrl()
    {
        return $this->_url;
    }

    public function getCurrentLine()
    {
        return $this->_currentLine;
    }
}

/**
 * The Originator. Creates a ConcreteMemento and returns it
 * as a Memento, taking back the Memento and checking it
 * to rebuild its internal state.
 * This class responsibility is to take an url and
 * returning one line from it at the time. Its work is
 * often broken into multiple [Ajax] requests, depending
 * on the parts the user wants to see.
 */
class Service
{
    protected $_url;
    protected $_currentLine = 0;

    public function __construct($url = null)
    {
        $this->_url = $url;
    }

    public function init()
    {
        $this->_content = file($this->_url);
    }

    /**
     * @return ConcreteMemento
     */
    public function getState()
    {
        return new ConcreteMemento($this->_url, $this->_currentLine);
    }

    public function setState(ConcreteMemento $state)
    {
        if (!($state instanceof Memento)) {
            throw new Exception('Memento object not recognized.');
        }
        $this->_url= $state->getUrl();
        $this->_currentLine = $state->getCurrentLine();
        $this->init();
    }

    public function getLine()
    {
        $line = $this->_content[$this->_currentLine];
        $this->_currentLine++;
        return $line;
    }
}

//Caretaker code
$service = new Service('http://giorgiosironi.blogspot.com');
$service->init();
echo $service->getLine();
echo $service->getLine();
echo $service->getLine();
$memento = $service->getState();
// now we can store Memento in the session
// let's simulate its handling
$mementoString = serialize($memento);
$newService = new Service();
$newService->setState(unserialize($mementoString));
echo $newService->getLine();
echo $newService->getLine();
echo $newService->getLine();

No comments:

ShareThis