3.6. Memento

3.6.1. Propósito

It provides the ability to restore an object to it’s previous state (undo via rollback) or to gain access to state of the object, without revealing it’s implementation (i.e., the object is not required to have a function to return the current state).

El patrón Memento se implementa con tres objetos: el Originador, el Conserje y el Memento.

Memento - un objeto que contiene una instantánea concreta y única del estado*de cualquier objeto o recurso: cadena, número, array, una instancia de clasey así sucesivamente. La singularidad en este caso no implica la prohibición de la existencia de estados similares en diferentes instantáneas. Eso significa que el estado se puede extraer como clon independiente. Cualquier objeto almacenado en Memento debe ser *una copia completa del objeto original en lugar de unreferencia al objeto original. El objeto Memento es un «objeto opaco» (el objeto que nadie puede ni debe cambiar).

Originador: es un objeto que contiene el estado actual de un objeto externo de tipo estrictamente especificado. El originador puede crear una copia única de este estado y la devuelve envuelta en un Memento. El Originador no conoce el historial de cambios. Puede establecer un estado concreto para el Originador desde el exterior, que se considerará como actual. El Originador debe asegurar que el estado dado corresponda al tipo de objeto permitido. El originador puede (pero no debería) tener cualquier método, pero no pueden realizar cambios en el estado del objeto guardado.

El Conserje controla el historial de estados. Puede realizar cambios en un objeto; tomar la decisión de guardar el estado de un objeto externo en el Originador; preguntar desde la instantánea del Originador del estado actual; o establecer el Estado del Originador a la equivalencia con alguna instantánea de la historia.

3.6.2. Ejemplos

  • La semilla de un generador de números pseudoaleatorios

  • El estado de una máquina de estado finita

  • Control para estados intermedios del Modelo ORM https://es.wikipedia.org/wiki/Asignaci%C3%B3n_objeto-relacional _ antes de guardar

3.6.3. Diagrama UML

Alt Momento UML Diagram

3.6.4. Código

Puedes encontrar el código en GitHub

Memento.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Memento;
 6
 7class Memento
 8{
 9    public function __construct(private State $state)
10    {
11    }
12
13    public function getState(): State
14    {
15        return $this->state;
16    }
17}

State.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Memento;
 6
 7use InvalidArgumentException;
 8
 9class State implements \Stringable
10{
11    public const STATE_CREATED = 'created';
12    public const STATE_OPENED = 'opened';
13    public const STATE_ASSIGNED = 'assigned';
14    public const STATE_CLOSED = 'closed';
15
16    private string $state;
17
18    /**
19     * @var string[]
20     */
21    private static array $validStates = [
22        self::STATE_CREATED,
23        self::STATE_OPENED,
24        self::STATE_ASSIGNED,
25        self::STATE_CLOSED,
26    ];
27
28    public function __construct(string $state)
29    {
30        self::ensureIsValidState($state);
31
32        $this->state = $state;
33    }
34
35    private static function ensureIsValidState(string $state)
36    {
37        if (!in_array($state, self::$validStates)) {
38            throw new InvalidArgumentException('Invalid state given');
39        }
40    }
41
42    public function __toString(): string
43    {
44        return $this->state;
45    }
46}

Ticket.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Memento;
 6
 7/**
 8 * Ticket is the "Originator" in this implementation
 9 */
10class Ticket
11{
12    private State $currentState;
13
14    public function __construct()
15    {
16        $this->currentState = new State(State::STATE_CREATED);
17    }
18
19    public function open()
20    {
21        $this->currentState = new State(State::STATE_OPENED);
22    }
23
24    public function assign()
25    {
26        $this->currentState = new State(State::STATE_ASSIGNED);
27    }
28
29    public function close()
30    {
31        $this->currentState = new State(State::STATE_CLOSED);
32    }
33
34    public function saveToMemento(): Memento
35    {
36        return new Memento(clone $this->currentState);
37    }
38
39    public function restoreFromMemento(Memento $memento)
40    {
41        $this->currentState = $memento->getState();
42    }
43
44    public function getState(): State
45    {
46        return $this->currentState;
47    }
48}

3.6.5. Test

Tests/MementoTest.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Memento\Tests;
 6
 7use DesignPatterns\Behavioral\Memento\State;
 8use DesignPatterns\Behavioral\Memento\Ticket;
 9use PHPUnit\Framework\TestCase;
10
11class MementoTest extends TestCase
12{
13    public function testOpenTicketAssignAndSetBackToOpen()
14    {
15        $ticket = new Ticket();
16
17        // open the ticket
18        $ticket->open();
19        $openedState = $ticket->getState();
20        $this->assertSame(State::STATE_OPENED, (string) $ticket->getState());
21
22        $memento = $ticket->saveToMemento();
23
24        // assign the ticket
25        $ticket->assign();
26        $this->assertSame(State::STATE_ASSIGNED, (string) $ticket->getState());
27
28        // now restore to the opened state, but verify that the state object has been cloned for the memento
29        $ticket->restoreFromMemento($memento);
30
31        $this->assertSame(State::STATE_OPENED, (string) $ticket->getState());
32        $this->assertNotSame($openedState, $ticket->getState());
33    }
34}