3.6. Memento

3.6.1. Purpose

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).

Das Memento-Muster wird mit drei Objekten umgesetzt: dem Urheber, einem Verwalter und einem Memento.

Memento - ein Objekt, das eine konkrete, eindeutige Momentaufnahme des Zustands eines beliebigen Objekts oder einer beliebigen Ressource enthält: Zeichenfolge, Nummer, Array, eine Instanz einer Klasse usw. Die Eindeutigkeit impliziert in diesem Fall nicht das Verbot der Existenz ähnlicher Zustände in verschiedenen Momentaufnahmen. Das bedeutet, dass der Zustand als der unabhängige Klon extrahiert werden kann.Jedes Objekt, das im Memento gespeichert ist, sollte eine vollständige Kopie des Originalobjekts und nicht eine Referenz auf das Originalobjekt sein. Das Memento-Objekt ist ein ‚undurchsichtiges Objekt‘ (ein Objekt, das niemand ändern kann oder sollte).

Urheber - es handelt sich um ein Objekt, das den tatsächlichen Zustand eines externen Objekts enthält, dessen Typ streng spezifiziert ist. Der Urheber ist in der Lage, eine eindeutige Kopie dieses Zustands zu erstellen und in einem Memento verpackt zurückzugeben. Der Absender kennt die Geschichte der Änderungen nicht. Sie können von außen einen konkreten Zustand auf Urheber setzen, der als aktuell betrachtet wird. Der Absender muss sicherstellen, dass der angegebene Zustand dem zulässigen Objekttyp entspricht. Der Urheber kann (sollte aber nicht) über beliebige Methoden verfügen, kann aber keine Änderungen am gespeicherten Objektzustand vornehmen.

Verwalter kontrollieren die Historie der Status. Er kann Änderungen an einem Objekt vornehmen, eine Entscheidung treffen, den Zustand eines externen Objekts im Urheber speichern, vom Urheber eine Momentaufnahme des aktuellen Zustands anfordern oder den Urheber-Zustand auf Gleichstand mit einer Momentaufnahme aus der Historie setzen.

3.6.2. Beispiele

3.6.3. UML Diagram

Alt Momento UML Diagram

3.6.4. Code

Du kannst diesen Code auch auf GitHub einsehen

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