3.6. Memento (Lembrança)

3.6.1. Objetivo

Ele provê a habilidade de restaurar um objeto para seu estado anterior (desfazer via rollback) ou ganhar acesso ao estado do objeto sem revelar sua implementação (p.e. o objeto não é obrigado a ter uma funcionalidade para retornar ao estado atual).

O padrão Memento é implementado com três objetos: o Originator, um Caretaker e um Memento.

Memento - um objeto que contém um snapshot único e concreto do estado de qualquer objeto ou recurso: string, número, array, uma instance de classe e assim por diante. A singularidade, neste caso, não implica a proibição da existência de estados semelhantes em diferentes snapshots. Isso significa que o estado pode ser extraído como o clone independente. Qualquer objeto armazenado no Memento deve ser uma cópia completa do objeto original em vez de uma referência para o objeto original. O objeto Memento é um “objeto opaco” (o objeto que ninguém pode ou deve mudar).

Originator - é um objeto que contém o estado atual de um objeto externo é estritamente o tipo especificado. Originator é capaz de criar uma cópia única deste estado e devolvê-lo envolto em um Memento. O O Originator não conhece a história das mudanças. Você pode definir um estado concreto ao Originator do lado de fora, que será considerado como atual. O Originator deve certificar-se de que determinado estado corresponde ao tipo permitido de objeto. Originator pode (mas não deve) ter quaisquer métodos, mas eles não podem fazer alterações no estado do objeto salvo.

Caretaker controla a história dos estados. Ele pode fazer alterações em um objeto; tomar uma decisão para salvar o estado de um objeto externo no Originator; pedir a partir do snapshot do Originator do estado atual ou definir o estado do Originator para equivalência com algum snapshot do histórico.

3.6.2. Exemplos

  • A semente de um gerador de números pseudo-aleatórios
  • O estado em uma máquina de estados finitos
  • Controle para estados intermediários de ORM Model antes de salvar

3.6.3. Diagrama UML

Alt Momento UML Diagram

3.6.4. Código

Você também pode encontrar este código no 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. Teste

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());
    }
}