3.6. Memento

3.6.1. Scopo

Permette di ripristinare lo stato precedenti di un oggetto (annullamento tramite rollback) o di ottenere accesso allo stato di quest’ultimo senza rivelarne l’implementazione (ad esempio all’oggetto non è richiesta una funzione per restituire il suo stato corrente).

Il pattern Memento è implementato con tre oggetti: Originator, Caretaker e Memento.

Memento - oggetto contenente un’istantanea concreta ed unica dello stato di un oggetto o di una risorsa: stringhe, numeri, array, istanze di una classe e così via. Unicità dello stato non implica l’inesistenza di stati simili in diffenti instantanee. Questo comporta che lo stato può essere estratto come un clone indipendente. Qualunque oggetto memorizzato nel Memento potrebbe essere una copia completa dell’oggetto originale invece di un riferimento. L’oggetto Memento è un «oggetto opaco» (che può o dovrebbe cambiare).

Originator - oggetto contenente lo stato attuale di un oggetto esterno di uno specifico tipo.L’Originator è in grado di creare una copia unica di questo stato e restituirlo incapsulato in un Memento. Non conosce lo storico dei cambiamenti di stato. Potete assegnargli uno stato concreto dall’esterno rendendolo così quello attuale. L’Originator deve assicurarsi che lo stato assegnatosia compatibile con quello permesso dal tipo di oggetto. Può (ma non dovrebbe) avere qualche metodo ma non possono effettuare cambiamenti allo stato dell’oggetto salvato.

Caretaker - controlla lo storico degli stati. Può effettuare dei cambiamenti all’oggetto, decidere se salvare lo stato di un oggetto esterno nell’Originator, chiedere a quest’ultimo un’istantanea dello stato corrente o cambiare lo stato corrente dell’Originator con uno dello storico delle instantanee.

3.6.2. Esempi

3.6.3. Diagramma UML

Alt Momento UML Diagram

3.6.4. Codice

Potete trovare questo codice anche su 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}