Laravel 13 备忘录模式深度解析

备忘录模式是一种行为型设计模式,它允许在不暴露对象实现细节的情况下保存和恢复对象的内部状态。本文将深入探讨 Laravel 13 中备忘录模式的高级用法。

备忘录模式基础

什么是备忘录模式

备忘录模式提供了一种方式来捕获对象的内部状态,并在之后将其恢复,同时不违反封装原则。

1
2
3
4
5
6
7
8
9
10
<?php

namespace App\Contracts;

interface MementoInterface
{
public function getState(): array;

public function getTimestamp(): \DateTime;
}
1
2
3
4
5
6
7
8
9
10
<?php

namespace App\Contracts;

interface OriginatorInterface
{
public function save(): MementoInterface;

public function restore(MementoInterface $memento): void;
}

基础备忘录实现

备忘录类

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

namespace App\Mementos;

use App\Contracts\MementoInterface;

class SnapshotMemento implements MementoInterface
{
protected array $state;
protected \DateTime $timestamp;
protected string $name;

public function __construct(string $name, array $state)
{
$this->name = $name;
$this->state = $state;
$this->timestamp = new \DateTime();
}

public function getState(): array
{
return $this->state;
}

public function getTimestamp(): \DateTime
{
return $this->timestamp;
}

public function getName(): string
{
return $this->name;
}
}

看护者类

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
50
51
52
53
54
55
56
57
<?php

namespace App\Caretakers;

use App\Contracts\MementoInterface;
use Illuminate\Support\Collection;

class HistoryCaretaker
{
protected Collection $history;
protected int $maxHistory;

public function __construct(int $maxHistory = 50)
{
$this->history = collect();
$this->maxHistory = $maxHistory;
}

public function push(MementoInterface $memento): void
{
$this->history->push($memento);

if ($this->history->count() > $this->maxHistory) {
$this->history->shift();
}
}

public function pop(): ?MementoInterface
{
return $this->history->pop();
}

public function get(int $index): ?MementoInterface
{
return $this->history->get($index);
}

public function getLatest(): ?MementoInterface
{
return $this->history->last();
}

public function getHistory(): Collection
{
return $this->history;
}

public function count(): int
{
return $this->history->count();
}

public function clear(): void
{
$this->history = collect();
}
}

文本编辑器备忘录

编辑器状态

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

namespace App\Mementos\Editor;

use App\Contracts\MementoInterface;

class EditorMemento implements MementoInterface
{
protected string $content;
protected int $cursorPosition;
protected array $selection;
protected \DateTime $timestamp;

public function __construct(string $content, int $cursorPosition, array $selection = [])
{
$this->content = $content;
$this->cursorPosition = $cursorPosition;
$this->selection = $selection;
$this->timestamp = new \DateTime();
}

public function getState(): array
{
return [
'content' => $this->content,
'cursor_position' => $this->cursorPosition,
'selection' => $this->selection,
];
}

public function getTimestamp(): \DateTime
{
return $this->timestamp;
}

public function getContent(): string
{
return $this->content;
}

public function getCursorPosition(): int
{
return $this->cursorPosition;
}

public function getSelection(): array
{
return $this->selection;
}
}

文本编辑器

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
<?php

namespace App\Services;

use App\Contracts\MementoInterface;
use App\Contracts\OriginatorInterface;
use App\Mementos\Editor\EditorMemento;
use App\Caretakers\HistoryCaretaker;

class TextEditor implements OriginatorInterface
{
protected string $content = '';
protected int $cursorPosition = 0;
protected array $selection = [];
protected HistoryCaretaker $history;

public function __construct()
{
$this->history = new HistoryCaretaker(100);
}

public function type(string $text): void
{
$this->save();

$before = substr($this->content, 0, $this->cursorPosition);
$after = substr($this->content, $this->cursorPosition);

$this->content = $before . $text . $after;
$this->cursorPosition += strlen($text);
}

public function delete(int $length = 1): void
{
$this->save();

$before = substr($this->content, 0, $this->cursorPosition - $length);
$after = substr($this->content, $this->cursorPosition);

$this->content = $before . $after;
$this->cursorPosition = max(0, $this->cursorPosition - $length);
}

public function backspace(int $length = 1): void
{
$this->delete($length);
}

public function setCursorPosition(int $position): void
{
$this->cursorPosition = max(0, min($position, strlen($this->content)));
}

public function select(int $start, int $end): void
{
$this->selection = [
'start' => max(0, $start),
'end' => min($end, strlen($this->content)),
];
}

public function clearSelection(): void
{
$this->selection = [];
}

public function getContent(): string
{
return $this->content;
}

public function getCursorPosition(): int
{
return $this->cursorPosition;
}

public function save(): MementoInterface
{
$memento = new EditorMemento(
$this->content,
$this->cursorPosition,
$this->selection
);

$this->history->push($memento);

return $memento;
}

public function restore(MementoInterface $memento): void
{
$state = $memento->getState();

$this->content = $state['content'];
$this->cursorPosition = $state['cursor_position'];
$this->selection = $state['selection'];
}

public function undo(): bool
{
$memento = $this->history->pop();

if ($memento) {
$this->restore($memento);
return true;
}

return false;
}

public function canUndo(): bool
{
return $this->history->count() > 0;
}

public function getHistory(): array
{
return $this->history->getHistory()->map(function ($memento) {
return [
'timestamp' => $memento->getTimestamp()->format('Y-m-d H:i:s'),
'content_preview' => substr($memento->getContent(), 0, 50),
];
})->toArray();
}
}

表单状态备忘录

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
50
51
52
53
<?php

namespace App\Mementos\Form;

use App\Contracts\MementoInterface;

class FormMemento implements MementoInterface
{
protected string $formId;
protected array $data;
protected int $step;
protected array $errors;
protected \DateTime $timestamp;

public function __construct(string $formId, array $data, int $step = 1, array $errors = [])
{
$this->formId = $formId;
$this->data = $data;
$this->step = $step;
$this->errors = $errors;
$this->timestamp = new \DateTime();
}

public function getState(): array
{
return [
'form_id' => $this->formId,
'data' => $this->data,
'step' => $this->step,
'errors' => $this->errors,
];
}

public function getTimestamp(): \DateTime
{
return $this->timestamp;
}

public function getFormId(): string
{
return $this->formId;
}

public function getData(): array
{
return $this->data;
}

public function getStep(): int
{
return $this->step;
}
}

多步骤表单

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
<?php

namespace App\Services;

use App\Contracts\MementoInterface;
use App\Contracts\OriginatorInterface;
use App\Mementos\Form\FormMemento;
use App\Caretakers\HistoryCaretaker;
use Illuminate\Support\Facades\Session;

class MultiStepForm implements OriginatorInterface
{
protected string $formId;
protected array $data = [];
protected int $currentStep = 1;
protected int $totalSteps;
protected HistoryCaretaker $history;

public function __construct(string $formId, int $totalSteps = 3)
{
$this->formId = $formId;
$this->totalSteps = $totalSteps;
$this->history = new HistoryCaretaker(20);

$this->loadFromSession();
}

public function setStepData(int $step, array $data): void
{
$this->save();

$this->data["step_{$step}"] = $data;
$this->saveToSession();
}

public function getStepData(int $step): array
{
return $this->data["step_{$step}"] ?? [];
}

public function nextStep(): bool
{
if ($this->currentStep < $this->totalSteps) {
$this->save();
$this->currentStep++;
$this->saveToSession();
return true;
}

return false;
}

public function previousStep(): bool
{
if ($this->currentStep > 1) {
$this->save();
$this->currentStep--;
$this->saveToSession();
return true;
}

return false;
}

public function goToStep(int $step): bool
{
if ($step >= 1 && $step <= $this->totalSteps) {
$this->save();
$this->currentStep = $step;
$this->saveToSession();
return true;
}

return false;
}

public function getCurrentStep(): int
{
return $this->currentStep;
}

public function getTotalSteps(): int
{
return $this->totalSteps;
}

public function getAllData(): array
{
return $this->data;
}

public function save(): MementoInterface
{
$memento = new FormMemento(
$this->formId,
$this->data,
$this->currentStep
);

$this->history->push($memento);

return $memento;
}

public function restore(MementoInterface $memento): void
{
$state = $memento->getState();

$this->data = $state['data'];
$this->currentStep = $state['step'];
$this->saveToSession();
}

public function undo(): bool
{
$memento = $this->history->pop();

if ($memento) {
$this->restore($memento);
return true;
}

return false;
}

public function reset(): void
{
$this->data = [];
$this->currentStep = 1;
$this->history->clear();
Session::forget($this->getSessionKey());
}

protected function saveToSession(): void
{
Session::put($this->getSessionKey(), [
'data' => $this->data,
'current_step' => $this->currentStep,
]);
}

protected function loadFromSession(): void
{
$saved = Session::get($this->getSessionKey());

if ($saved) {
$this->data = $saved['data'] ?? [];
$this->currentStep = $saved['current_step'] ?? 1;
}
}

protected function getSessionKey(): string
{
return "form.{$this->formId}";
}
}

游戏状态备忘录

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
50
51
52
53
54
55
56
<?php

namespace App\Mementos\Game;

use App\Contracts\MementoInterface;

class GameStateMemento implements MementoInterface
{
protected int $level;
protected int $score;
protected int $lives;
protected array $inventory;
protected array $position;
protected \DateTime $timestamp;

public function __construct(
int $level,
int $score,
int $lives,
array $inventory,
array $position
) {
$this->level = $level;
$this->score = $score;
$this->lives = $lives;
$this->inventory = $inventory;
$this->position = $position;
$this->timestamp = new \DateTime();
}

public function getState(): array
{
return [
'level' => $this->level,
'score' => $this->score,
'lives' => $this->lives,
'inventory' => $this->inventory,
'position' => $this->position,
];
}

public function getTimestamp(): \DateTime
{
return $this->timestamp;
}

public function getLevel(): int
{
return $this->level;
}

public function getScore(): int
{
return $this->score;
}
}

游戏角色

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
<?php

namespace App\Services\Game;

use App\Contracts\MementoInterface;
use App\Contracts\OriginatorInterface;
use App\Mementos\Game\GameStateMemento;
use Illuminate\Support\Facades\Cache;

class GameCharacter implements OriginatorInterface
{
protected string $playerId;
protected int $level = 1;
protected int $score = 0;
protected int $lives = 3;
protected array $inventory = [];
protected array $position = ['x' => 0, 'y' => 0];

public function __construct(string $playerId)
{
$this->playerId = $playerId;
}

public function move(int $x, int $y): void
{
$this->position = ['x' => $x, 'y' => $y];
}

public function addItem(string $item): void
{
$this->inventory[] = $item;
}

public function removeItem(string $item): bool
{
$index = array_search($item, $this->inventory);

if ($index !== false) {
unset($this->inventory[$index]);
$this->inventory = array_values($this->inventory);
return true;
}

return false;
}

public function addScore(int $points): void
{
$this->score += $points;
}

public function loseLife(): bool
{
$this->lives--;

return $this->lives > 0;
}

public function levelUp(): void
{
$this->level++;
}

public function save(): MementoInterface
{
$memento = new GameStateMemento(
$this->level,
$this->score,
$this->lives,
$this->inventory,
$this->position
);

Cache::put($this->getSaveKey(), $memento, 86400);

return $memento;
}

public function restore(MementoInterface $memento): void
{
$state = $memento->getState();

$this->level = $state['level'];
$this->score = $state['score'];
$this->lives = $state['lives'];
$this->inventory = $state['inventory'];
$this->position = $state['position'];
}

public function loadSavedGame(): bool
{
$memento = Cache::get($this->getSaveKey());

if ($memento) {
$this->restore($memento);
return true;
}

return false;
}

public function getLevel(): int
{
return $this->level;
}

public function getScore(): int
{
return $this->score;
}

public function getLives(): int
{
return $this->lives;
}

public function getInventory(): array
{
return $this->inventory;
}

public function getPosition(): array
{
return $this->position;
}

protected function getSaveKey(): string
{
return "game.save.{$this->playerId}";
}
}

数据库事务备忘录

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

namespace App\Mementos\Database;

use App\Contracts\MementoInterface;

class TransactionMemento implements MementoInterface
{
protected string $transactionId;
protected array $queries;
protected array $snapshots;
protected \DateTime $timestamp;

public function __construct(string $transactionId, array $queries, array $snapshots)
{
$this->transactionId = $transactionId;
$this->queries = $queries;
$this->snapshots = $snapshots;
$this->timestamp = new \DateTime();
}

public function getState(): array
{
return [
'transaction_id' => $this->transactionId,
'queries' => $this->queries,
'snapshots' => $this->snapshots,
];
}

public function getTimestamp(): \DateTime
{
return $this->timestamp;
}

public function getQueries(): array
{
return $this->queries;
}

public function getSnapshots(): array
{
return $this->snapshots;
}
}

测试备忘录模式

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
50
51
52
53
54
55
56
57
58
59
60
<?php

namespace Tests\Unit\Mementos;

use Tests\TestCase;
use App\Services\TextEditor;
use App\Services\MultiStepForm;

class MementoTest extends TestCase
{
public function test_text_editor_undo(): void
{
$editor = new TextEditor();

$editor->type('Hello');
$editor->type(' World');
$editor->type('!');

$this->assertEquals('Hello World!', $editor->getContent());

$editor->undo();
$this->assertEquals('Hello World', $editor->getContent());

$editor->undo();
$this->assertEquals('Hello', $editor->getContent());
}

public function test_text_editor_delete(): void
{
$editor = new TextEditor();

$editor->type('Hello World');
$editor->setCursorPosition(5);
$editor->delete(6);

$this->assertEquals('Hello', $editor->getContent());

$editor->undo();
$this->assertEquals('Hello World', $editor->getContent());
}

public function test_multi_step_form(): void
{
$form = new MultiStepForm('test_form', 3);

$form->setStepData(1, ['name' => 'John']);
$form->nextStep();

$form->setStepData(2, ['email' => 'john@example.com']);
$form->nextStep();

$this->assertEquals(3, $form->getCurrentStep());

$form->undo();
$this->assertEquals(2, $form->getCurrentStep());

$form->previousStep();
$this->assertEquals(1, $form->getCurrentStep());
}
}

最佳实践

1. 备忘录应该不可变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

class ImmutableMemento implements MementoInterface
{
protected readonly array $state;
protected readonly \DateTime $timestamp;

public function __construct(array $state)
{
$this->state = $state;
$this->timestamp = new \DateTime();
}

public function getState(): array
{
return $this->state;
}

public function getTimestamp(): \DateTime
{
return $this->timestamp;
}
}

2. 序列化支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

class SerializableMemento implements MementoInterface, \Serializable
{
protected array $state;

public function serialize(): string
{
return serialize($this->state);
}

public function unserialize(string $data): void
{
$this->state = unserialize($data);
}
}

3. 增量备忘录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

class IncrementalMemento implements MementoInterface
{
protected array $changes;
protected string $baseVersion;

public function __construct(string $baseVersion, array $changes)
{
$this->baseVersion = $baseVersion;
$this->changes = $changes;
}

public function getState(): array
{
return [
'base_version' => $this->baseVersion,
'changes' => $this->changes,
];
}
}

总结

Laravel 13 的备忘录模式提供了一种优雅的方式来保存和恢复对象状态。通过合理使用备忘录模式,可以实现撤销/重做功能、状态持久化和事务回滚等功能,同时保持对象的封装性。