diff --git a/Behavioral/Memento/Caretaker.php b/Behavioral/Memento/Caretaker.php index 340bb97..3a16fcd 100644 --- a/Behavioral/Memento/Caretaker.php +++ b/Behavioral/Memento/Caretaker.php @@ -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(); } + } diff --git a/Behavioral/Memento/Originator.php b/Behavioral/Memento/Originator.php index fec4e45..3acb0c1 100644 --- a/Behavioral/Memento/Originator.php +++ b/Behavioral/Memento/Originator.php @@ -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); diff --git a/Behavioral/Memento/README.rst b/Behavioral/Memento/README.rst index 6696149..2cbc555 100644 --- a/Behavioral/Memento/README.rst +++ b/Behavioral/Memento/README.rst @@ -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 `_ before saving UML Diagram ----------- diff --git a/Behavioral/Memento/Tests/MementoTest.php b/Behavioral/Memento/Tests/MementoTest.php index 88110a6..ccc2097 100644 --- a/Behavioral/Memento/Tests/MementoTest.php +++ b/Behavioral/Memento/Tests/MementoTest.php @@ -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); } } diff --git a/Behavioral/Memento/uml/uml.png b/Behavioral/Memento/uml/uml.png index e96ea69..0fde074 100644 Binary files a/Behavioral/Memento/uml/uml.png and b/Behavioral/Memento/uml/uml.png differ