备忘录模式

目的

备忘录模式,可以在不暴露对象具体实现细节的情况下,提供了将一个对象恢复到之前的状态(通过回滚来取消)或获取对象状态的能力(即:不要求对象有返回当前状态的方法)。

该模式由三个对象构成:OriginatorCaretakerMemento

  • Memento:是一个对象,它包含任何对象(或资源)的具体的唯一状态快照:字符串、数字、数组、类实例等。在这种情况下,唯一性并不意味着禁止在不同快照中存在类似的状态,而是指状态可以作为独立的克隆体提取出来。存储在 Memento 中的任何对象都应该是原始对象的完整副本,而不是对原始对象的引用。Memento 对象是一个“不透明的对象”,任何人都不能也不应该改变这个对象。
  • Originator:它是一个包含了严格类型的外部对象实际状态的对象。Originator 能够创建该状态的唯一副本,并将其包裹在 Memento 中返回。Originator 并不了解状态变化的历史。你可以从外部给 Originator 设置一个具体的状态,这将被视为实际状态。Originator 必须确保给定的状态与允许的对象类型相对应。Originator 可以(但不应该)拥有任何方法,但他们不能对已保存的对象的状态进行更改。
  • Caretaker:该对象用于控制状态历史。它可以对一个对象的状态进行修改;也可以决定将外部对象的状态保存在 Originator 中;还可以获取 Originator 当前状态的快照;或者,将 Originator 的状态设置为与历史中的某个快照一样的状态。

使用场景

  • 伪随机数生成器的种子
  • 有限状态机中的状态
  • ORM 模型保存数据前中间状态的控制

UML 类图

备忘录模式类图

代码

Memento.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php declare(strict_types = 1);

namespace DesignPatterns\Behavioral\Memento;

class Memento
{
private State $state;

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

public function getState(): State
{
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
<?php declare(strict_types = 1);

namespace DesignPatterns\Behavioral\Memento;

use InvalidArgumentException;

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

private string $state;

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

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
<?php declare(strict_types = 1);

namespace DesignPatterns\Behavioral\Memento;

/**
* Ticket is the "Originator" in this implementation
*/
class Ticket
{
private State $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;
}
}

测试

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 declare(strict_types = 1);

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

$memento = $ticket->saveToMemento();

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