3.6. Знімок (Memento)

3.6.1. Призначення

Патерн надає можливість відновити об’єкт у його попередньому стані (скасувати дію за допомогою відкату до попереднього стану) або отримати доступ до стану об’єкта, не розкриваючи його реалізацію (тобто. сам об’єкт не повинен мати функціональність для повернення поточного стану).

Шаблон Зберігач реалізується трьома об’єктами: «Творцем» (originator), «Опікуном» (caretaker) і «Охоронець» (memento).

Знімок - це об’єкт, який зберігає конкретний знімок стану деякого об’єкта чи ресурсу: рядки, числа, масиву, екземпляра класу і так далі. Унікальність у даному випадку має на увазі не заборону на існування однакових станів у різних знімках, а те, що стан можна витягти у вигляді незалежної копії. Будь-який об’єкт, що зберігається в Знімок повинен бути повною копією вихідного об’єкта, а не посиланням на вихідний об’єкт. Сам об’єкт Знімок є «непрозорим об’єктом» (той, який ніхто не може і не повинен змінювати).

Творець - це об’єкт, який містить у собі актуальний стан зовнішнього об’єкта строго заданого типу* і вміє створювати унікальну копію цього стану, повертаючи її, обгорнуту в об’єкт Знімок. Творець не знає історії змін. Творцю можна примусово встановити конкретний стан ззовні, яка буде вважатися актуальною. Творець повинен подбати про те, щоб цей стан відповідав типу об’єкта, з яким йому дозволено працювати. Творець може (але не зобов’язаний) мати будь-які методи, але вони не можуть змінити збережений стан об’єкта.

Опікун керує історією знімків станів. Він може вносити зміни до об’єкту, приймати рішення про збереження стану зовнішнього об’єкта у Творці,вимагати від Творця знімок поточного стану, або привести стан Творця у відповідність до стану якогось знімка з історії.

3.6.2. Приклади

  • Зерно генератора псевдовипадковихчисел.

  • Стан кінцевого автомату

  • Контроль проміжних станів ORM моделі перед збереженням

3.6.3. Діаграма UML

Alt Momento UML Diagram

3.6.4. Код

Ви можете знайти цей код на 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. Тест

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}