3.5. Pamiątka (Memento)

3.5.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.5.2. Przykłady

3.5.3. Diagram UML

Alt Momento UML Diagram

3.5.4. Kod

Ten kod znajdziesz również na GitHub.

Memento.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
<?php

namespace DesignPatterns\Behavioral\Memento;

class Memento
{
    /**
     * @var State
     */
    private $state;

    /**
     * @param State $stateToSave
     */
    public function __construct(State $stateToSave)
    {
        $this->state = $stateToSave;
    }

    /**
     * @return State
     */
    public function getState()
    {
        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
47
48
<?php

namespace DesignPatterns\Behavioral\Memento;

class State
{
    const STATE_CREATED = 'created';
    const STATE_OPENED = 'opened';
    const STATE_ASSIGNED = 'assigned';
    const STATE_CLOSED = 'closed';

    /**
     * @var string
     */
    private $state;

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

    /**
     * @param string $state
     */
    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
49
<?php

namespace DesignPatterns\Behavioral\Memento;

/**
 * Ticket is the "Originator" in this implementation
 */
class Ticket
{
    /**
     * @var State
     */
    private $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.5.5. Testy

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
<?php

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->assertEquals(State::STATE_OPENED, (string) $ticket->getState());

        $memento = $ticket->saveToMemento();

        // assign the ticket
        $ticket->assign();
        $this->assertEquals(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->assertEquals(State::STATE_OPENED, (string) $ticket->getState());
        $this->assertNotSame($openedState, $ticket->getState());
    }
}