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}