Memento pattern.

- Caretaker should save history for normal pattern implementation.
- Importent notes in code of Originator.
- Readme with full pattern philosophy.
- Tests and example of usage inside test.
This commit is contained in:
Евгений Глотов
2015-05-30 03:58:34 +03:00
parent 4703828950
commit 0ebafc5eaa
5 changed files with 160 additions and 48 deletions

View File

@@ -4,12 +4,26 @@ namespace DesignPatterns\Behavioral\Memento;
class Caretaker
{
public static function run()
protected $history = array();
/**
* @return Memento
*/
public function getFromHistory($id)
{
/* @var $savedStates Memento[] */
return $this->history[$id];
}
$savedStates = array();
/**
* @param Memento $state
*/
public function saveToHistory(Memento $state)
{
$this->history[] = $state;
}
public function runCustomLogic()
{
$originator = new Originator();
//Setting state to State1
@@ -17,17 +31,20 @@ class Caretaker
//Setting state to State2
$originator->setState("State2");
//Saving State2 to Memento
$savedStates[] = $originator->saveToMemento();
$this->saveToHistory($originator->getStateAsMemento());
//Setting state to State3
$originator->setState("State3");
// We can request multiple mementos, and choose which one to roll back to.
// Saving State3 to Memento
$savedStates[] = $originator->saveToMemento();
$this->saveToHistory($originator->getStateAsMemento());
//Setting state to State4
$originator->setState("State4");
$originator->restoreFromMemento($savedStates[1]);
$originator->restoreFromMemento($this->getFromHistory(1));
//State after restoring from Memento: State3
return $originator->getStateAsMemento()->getState();
}
}

View File

@@ -15,14 +15,17 @@ class Originator
*/
public function setState($state)
{
// you must check type of state inside child of this class
// or use type-hinting for full pattern implementation
$this->state = $state;
}
/**
* @return Memento
*/
public function saveToMemento()
public function getStateAsMemento()
{
// you must save a separate copy in Memento
$state = is_object($this->state) ? clone $this->state : $this->state;
return new Memento($state);

View File

@@ -4,26 +4,43 @@
Purpose
-------
Provide the ability to restore an object to its previous state (undo via
rollback).
It provides the ability to restore an object to its previous state (undo
via rollback) or to gain access to state of the object, without revealing
its implementation (ie, the object is not required to have a functional
for return the current state).
The memento pattern is implemented with three objects: the originator, a
caretaker and a memento. The originator is some object that has an
internal state. The caretaker is going to do something to the
originator, but wants to be able to undo the change. The caretaker first
asks the originator for a memento object. Then it does whatever
operation (or sequence of operations) it was going to do. To roll back
to the state before the operations, it returns the memento object to the
originator. The memento object itself is an opaque object (one which the
caretaker cannot, or should not, change). When using this pattern, care
should be taken if the originator may change other objects or resources
- the memento pattern operates on a single object.
The memento pattern is implemented with three objects: the Originator, a
Caretaker and a Memento.
Memento an object that *contains a concrete unique snapshot of state* of
any object or resource: string, number, array, an instance of class and so on.
The uniqueness in this case not imply the prohibition existence of similar
states in different snapshots. That means the state can be extracted as
the independent clone. Any object stored in the Memento should be
*a full copy of the original object rather than a reference* to the original
object. The Memento object is a "opaque object" (the object that no one can
or should change).
Originator it is an object that contains the *actual state of an external
object is strictly specified type*. Originator is able to create a unique
copy of this state and return it wrapped in a Memento. The Originator does
not know the history of changes. You can set a concrete state to Originator
from the outside, which will be considered as actual. The Originator must
make sure that given state corresponds the allowed type of object. Originator
may (but not should) have any methods, but they *they can't make changes to
the saved object state*.
Caretaker *controls the states history*. He may make changes to an object;
take a decision to save the state of an external object in the Originator;
ask from the Originator snapshot of the current state; or set the Originator
state to equivalence with some snapshot from history.
Examples
--------
- The seed of a pseudorandom number generator
- The state in a finite state machine
- Control for intermediate states of `ORM Model <http://en.wikipedia.org/wiki/Object-relational_mapping>`_ before saving
UML Diagram
-----------

View File

@@ -2,6 +2,8 @@
namespace DesignPatterns\Behavioral\Memento\Tests;
use DesignPatterns\Behavioral\Memento\Caretaker;
use DesignPatterns\Behavioral\Memento\Memento;
use DesignPatterns\Behavioral\Memento\Originator;
/**
@@ -10,6 +12,37 @@ use DesignPatterns\Behavioral\Memento\Originator;
class MementoTest extends \PHPUnit_Framework_TestCase
{
public function testUsageExample()
{
$originator = new Originator();
$caretaker = new Caretaker();
$character = new \stdClass();
$character->name = "Gandalf"; // new object
$originator->setState($character); // connect Originator to character object
$character->name = "Gandalf the Grey"; // work on the object
$character->race = "Maia"; // still change something
$snapshot = $originator->getStateAsMemento(); // time to save state
$caretaker->saveToHistory($snapshot); // put state to log
$character->name = "Sauron"; // change something
$character->race = "Ainur"; // and again
$this->assertAttributeEquals($character, "state", $originator); // state inside the Originator was equally changed
$snapshot = $originator->getStateAsMemento(); // time to save another state
$caretaker->saveToHistory($snapshot); // put state to log
$rollback = $caretaker->getFromHistory(0);
$originator->restoreFromMemento($rollback); // return to first state
$character = $rollback->getState(); // use character from old state
$this->assertEquals("Gandalf the Grey", $character->name); // yes, that what we need
$character->name = "Gandalf the White"; // make new changes
$this->assertAttributeEquals($character, "state", $originator); // and Originator linked to actual object again
}
public function testStringState()
{
$originator = new Originator();
@@ -18,52 +51,94 @@ class MementoTest extends \PHPUnit_Framework_TestCase
$this->assertAttributeEquals("State1", "state", $originator);
$originator->setState("State2");
$this->assertAttributeEquals("State2", "state", $originator);
$savedState = $originator->saveToMemento();
$this->assertAttributeEquals("State2", "state", $savedState);
$snapshot = $originator->getStateAsMemento();
$this->assertAttributeEquals("State2", "state", $snapshot);
$originator->setState("State3");
$this->assertAttributeEquals("State3", "state", $originator);
$originator->restoreFromMemento($savedState);
$originator->restoreFromMemento($snapshot);
$this->assertAttributeEquals("State2", "state", $originator);
}
public function testObjectState()
public function testSnapshotIsClone()
{
$originator = new Originator();
$object = new \stdClass();
$originator->setState($object);
$snapshot = $originator->getStateAsMemento();
$object->new_property = 1;
$this->assertAttributeEquals($object, "state", $originator);
$this->assertAttributeNotEquals($object, "state", $snapshot);
$originator->restoreFromMemento($snapshot);
$this->assertAttributeNotEquals($object, "state", $originator);
}
public function testCanChangeActualState()
{
$originator = new Originator();
$first_state = new \stdClass();
$originator->setState($first_state);
$snapshot = $originator->getStateAsMemento();
$second_state = $snapshot->getState();
$first_state->first_property = 1; // still actual
$second_state->second_property = 2; // just history
$this->assertAttributeEquals($first_state, "state", $originator);
$this->assertAttributeNotEquals($second_state, "state", $originator);
$originator->restoreFromMemento($snapshot);
$first_state->first_property = 11; // now it lost state
$second_state->second_property = 22; // must be actual
$this->assertAttributeEquals($second_state, "state", $originator);
$this->assertAttributeNotEquals($first_state, "state", $originator);
}
public function testStateWithDifferentObjects()
{
$originator = new Originator();
$foo = new \stdClass();
$foo->data = "foo";
$first = new \stdClass();
$first->data = "foo";
$originator->setState($foo);
$originator->setState($first);
$this->assertAttributeEquals($first, "state", $originator);
$this->assertAttributeEquals($foo, "state", $originator);
$first_snapshot = $originator->getStateAsMemento();
$this->assertAttributeEquals($first, "state", $first_snapshot);
$savedState = $originator->saveToMemento();
$second = new \stdClass();
$second->data = "bar";
$originator->setState($second);
$this->assertAttributeEquals($second, "state", $originator);
$this->assertAttributeEquals($foo, "state", $savedState);
$originator->restoreFromMemento($first_snapshot);
$this->assertAttributeEquals($first, "state", $originator);
}
$bar = new \stdClass();
$bar->data = "bar";
public function testCaretaker()
{
$caretaker = new Caretaker();
$memento1 = new Memento("foo");
$memento2 = new Memento("bar");
$caretaker->saveToHistory($memento1);
$caretaker->saveToHistory($memento2);
$this->assertAttributeEquals(array($memento1, $memento2), "history", $caretaker);
$this->assertEquals($memento1, $caretaker->getFromHistory(0));
$this->assertEquals($memento2, $caretaker->getFromHistory(1));
$originator->setState($bar);
}
$this->assertAttributeEquals($bar, "state", $originator);
$originator->restoreFromMemento($savedState);
$this->assertAttributeEquals($foo, "state", $originator);
$foo->data = null;
$this->assertAttributeNotEquals($foo, "state", $savedState);
$this->assertAttributeNotEquals($foo, "state", $originator);
public function testCaretakerCustomLogic()
{
$caretaker = new Caretaker();
$result = $caretaker->runCustomLogic();
$this->assertEquals("State3", $result);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 61 KiB