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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php

declare(strict_types=1);

namespace DesignPatterns\Behavioral\Memento;

class Memento
{
    public function __construct(private State $state)
    {
    }

    public function getState(): State
    {
        return $this->state;
    }
}

State.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php

declare(strict_types=1);

namespace DesignPatterns\Behavioral\Memento;

use InvalidArgumentException;

class State implements \Stringable
{
    public const STATE_CREATED = 'created';
    public const STATE_OPENED = 'opened';
    public const STATE_ASSIGNED = 'assigned';
    public const STATE_CLOSED = 'closed';

    private string $state;

    /**
     * @var string[]
     */
    private static array $validStates = [
        self::STATE_CREATED,
        self::STATE_OPENED,
        self::STATE_ASSIGNED,
        self::STATE_CLOSED,
    ];

    public function __construct(string $state)
    {
        self::ensureIsValidState($state);

        $this->state = $state;
    }

    private static function ensureIsValidState(string $state)
    {
        if (!in_array($state, self::$validStates)) {
            throw new InvalidArgumentException('Invalid state given');
        }
    }

    public function __toString(): string
    {
        return $this->state;
    }
}

Ticket.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php

declare(strict_types=1);

namespace DesignPatterns\Behavioral\Memento;

/**
 * Ticket is the "Originator" in this implementation
 */
class Ticket
{
    private State $currentState;

    public function __construct()
    {
        $this->currentState = new State(State::STATE_CREATED);
    }

    public function open()
    {
        $this->currentState = new State(State::STATE_OPENED);
    }

    public function assign()
    {
        $this->currentState = new State(State::STATE_ASSIGNED);
    }

    public function close()
    {
        $this->currentState = new State(State::STATE_CLOSED);
    }

    public function saveToMemento(): Memento
    {
        return new Memento(clone $this->currentState);
    }

    public function restoreFromMemento(Memento $memento)
    {
        $this->currentState = $memento->getState();
    }

    public function getState(): State
    {
        return $this->currentState;
    }
}

3.6.5. Test

Tests/MementoTest.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php

declare(strict_types=1);

namespace DesignPatterns\Behavioral\Memento\Tests;

use DesignPatterns\Behavioral\Memento\State;
use DesignPatterns\Behavioral\Memento\Ticket;
use PHPUnit\Framework\TestCase;

class MementoTest extends TestCase
{
    public function testOpenTicketAssignAndSetBackToOpen()
    {
        $ticket = new Ticket();

        // open the ticket
        $ticket->open();
        $openedState = $ticket->getState();
        $this->assertSame(State::STATE_OPENED, (string) $ticket->getState());

        $memento = $ticket->saveToMemento();

        // assign the ticket
        $ticket->assign();
        $this->assertSame(State::STATE_ASSIGNED, (string) $ticket->getState());

        // now restore to the opened state, but verify that the state object has been cloned for the memento
        $ticket->restoreFromMemento($memento);

        $this->assertSame(State::STATE_OPENED, (string) $ticket->getState());
        $this->assertNotSame($openedState, $ticket->getState());
    }
}