3.6. Хранитель (Memento)
3.6.1. Назначение
Шаблон предоставляет возможность восстановить объект в его предыдущем состоянии (отменить действие посредством отката к предыдущему состоянию) или получить доступ к состоянию объекта, не раскрывая его реализацию (т.е. сам объект не обязан иметь функциональность для возврата текущего состояния).
Шаблон Хранитель реализуется тремя объектами: «Создателем» (originator), «Опекуном» (caretaker) и «Хранитель» (memento).
Хранитель - это объект, который хранит конкретный снимок состояния некоторого объекта или ресурса: строки, числа, массива, экземпляра класса и так далее. Уникальность в данном случае подразумевает не запрет на существование одинаковых состояний в разных снимках, а то, что состояние можно извлечь в виде независимой копии. Любой объект, сохраняемый в Хранителе, должен быть полной копией исходного объекта, а не ссылкой на исходный объект. Сам объект Хранитель является «непрозрачным объектом» (тот, который никто не может и не должен изменять).
Создатель — это объект, который содержит в себе актуальное состояние внешнего объекта строго заданного типа и умеет создавать уникальную копию этого состояния, возвращая её, обёрнутую в объект Хранителя. Создатель не знает истории изменений. Создателю можно принудительно установить конкретное состояние извне, которое будет считаться актуальным. Создатель должен позаботиться о том, чтобы это состояние соответствовало типу объекта, с которым ему разрешено работать. Создатель может (но не обязан) иметь любые методы, но они не могут менять сохранённое состояние объекта.
Опекун управляет историей снимков состояний. Он может вносить изменения в объект, принимать решение о сохранении состояния внешнего объекта в Создателе, запрашивать от Создателя снимок текущего состояния, или привести состояние Создателя в соответствие с состоянием какого-то снимка из истории.
3.6.2. Примеры
3.6.3. Диаграмма UML
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}