3.6. Pamiątka (Memento)

3.6.1. Przeznaczenie

Implementacja tego wzorca umożliwia przywrócenie obiektu do poprzedniego stanu (poprzez wycofanie zmian) lub uzyskaniu dostępu do stanu obiektu bez wiedzy na temat wewnętrznej implementacji obiektu (w sytuacji, gdy na przykład obiekt nie posiada metody pozwalającej na zwrócenie jego stanu).

Implementacja wzorca Pamiątki składa się z trzech obiektów: Twórca, Opiekun i Pamiątki.

Pamiątka (ang. Memento) - obiekt, który zawiera migawkę (ang. snapshot) pełnej, unikatowej informacji o stanie obiektu lub zasobu: napisu, liczby, tablicy lub instancji obiektu. Unikatowość w tym przypadku nie zabrania istnienia innych migawek podobnego stanu danego obiektu. Oznacza to, że stan może zostać pobrany z obiektu Memento jako niezależny klon. Jeżeli obiekt Memento przechowuje inny obiekt, powinien on być kopią pierwotnego obiektu, a nie referencją na niego. Obiekt Memento nie jest przezroczysty (opaque object), co oznacza, że nikt tego obiektu nie może lub nie powinien zmieniać.

Twórca (ang. Originator) - obiekt, który zawiera faktyczny stan zewnętrznego obiektu o określonym typie. Twórca jest w stanie stworzyć kopię stanu zewnętrznego obiektu i zwrócić go opakowanego w Pamiątkę. Twórca nie posiada informacji o historii zmian, natomiast można mu przekazać stan jaki ma zostać ustawiony zewnętrznemu obiektowi, który będzie traktowany jako aktualny. Twórca jest odpowiedzialny za sprawdzenie czy przekazany stan może zostać ustawiony zewnętrznemu obiektowi, sprawdzając jego typ. Twórca może posiadać, choć nie powinien, dodatkowe metody, ale te nie mogą zmieniać stanu zewnętrznego obiektu. Może je jedynie przywrócić z Pamiątki.

Opiekun (ang. Caretaker) - obiekt, który zarządza historią stanów obiektu. Może wprowadzać zmiany w obiekcie, zapisywać historię zewnętrznego obiektu poprzez Twórcę, prosić Twórcę o stworzenie Pamiątki (migawki stanu zewnętrznego obiektu) lub prosić Twórcę o ustawienie stanu zewnętrznego obiektu poprzez wskazanie Pamiątki z zachowanej historii.

3.6.2. Przykłady

3.6.3. Diagram UML

Alt Momento UML Diagram

3.6.4. Kod

Ten kod znajdziesz również na 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. Testy

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}