Memento
Memento — это поведенческий паттерн проектирования, который позволяет сохранять и восстанавливать предыдущее состояние объекта, не раскрывая деталей его реализации.
Проблема
Представьте, что вы создаете текстовый редактор. Пользователи должны иметь возможность отменять и повторять свои действия. Самый простой способ — сохранять состояние документа перед каждым изменением. Но это требует много памяти, особенно для больших документов.
Кроме того, вы не можете просто сохранить весь объект документа, потому что некоторые части могут быть приватными, и вы не хотите раскрывать их структуру.
Решение
Паттерн Memento предлагает создать объект-хранитель (memento), который будет содержать снимок состояния объекта. Этот объект можно сохранять и использовать для восстановления состояния.
Структура
Originator
├── state: string
├── createMemento(): Memento
└── restoreMemento(memento: Memento): void
Memento
├── state: string
├── getState(): string
└── setState(state: string): void
Caretaker
├── mementos: Memento[]
├── saveMemento(memento: Memento): void
├── getMemento(index: number): Memento
└── getMementosCount(): number
Пример реализации
JavaScript
// Хранитель состояния
class Memento {
constructor(state) {
this.state = state;
this.timestamp = new Date().toISOString();
}
getState() {
return this.state;
}
getTimestamp() {
return this.timestamp;
}
}
// Создатель состояния - текстовый редактор
class TextEditor {
constructor() {
this.content = '';
this.cursorPosition = 0;
this.selection = null;
}
setContent(content) {
this.content = content;
console.log(`Содержимое изменено: "${content}"`);
}
setCursorPosition(position) {
this.cursorPosition = position;
console.log(`Позиция курсора: ${position}`);
}
setSelection(start, end) {
this.selection = { start, end };
console.log(`Выделение: ${start}-${end}`);
}
getContent() {
return this.content;
}
getCursorPosition() {
return this.cursorPosition;
}
getSelection() {
return this.selection;
}
// Создание снимка состояния
createMemento() {
const state = {
content: this.content,
cursorPosition: this.cursorPosition,
selection: this.selection ? { ...this.selection } : null
};
return new Memento(state);
}
// Восстановление состояния
restoreMemento(memento) {
const state = memento.getState();
this.content = state.content;
this.cursorPosition = state.cursorPosition;
this.selection = state.selection;
console.log(`Состояние восстановлено из снимка от ${memento.getTimestamp()}`);
}
// Демонстрация текущего состояния
displayState() {
console.log(`Содержимое: "${this.content}"`);
console.log(`Курсор: ${this.cursorPosition}`);
console.log(`Выделение: ${this.selection ? `${this.selection.start}-${this.selection.end}` : 'нет'}`);
}
}
// Опекун - менеджер истории
class HistoryManager {
constructor() {
this.history = [];
this.currentIndex = -1;
this.maxHistorySize = 10;
}
saveMemento(memento) {
// Удаляем снимки после текущей позиции (если есть)
this.history = this.history.slice(0, this.currentIndex + 1);
// Добавляем новый снимок
this.history.push(memento);
this.currentIndex++;
// Ограничиваем размер истории
if (this.history.length > this.maxHistorySize) {
this.history.shift();
this.currentIndex--;
}
console.log(`Снимок сохранен. История: ${this.history.length} снимков`);
}
undo() {
if (this.currentIndex > 0) {
this.currentIndex--;
const memento = this.history[this.currentIndex];
console.log(`Откат к снимку от ${memento.getTimestamp()}`);
return memento;
} else {
console.log('Нет снимков для отката');
return null;
}
}
redo() {
if (this.currentIndex < this.history.length - 1) {
this.currentIndex++;
const memento = this.history[this.currentIndex];
console.log(`Повтор к снимку от ${memento.getTimestamp()}`);
return memento;
} else {
console.log('Нет снимков для повтора');
return null;
}
}
getCurrentMemento() {
if (this.currentIndex >= 0 && this.currentIndex < this.history.length) {
return this.history[this.currentIndex];
}
return null;
}
getHistorySize() {
return this.history.length;
}
getCurrentIndex() {
return this.currentIndex;
}
clearHistory() {
this.history = [];
this.currentIndex = -1;
console.log('История очищена');
}
}
// Использование
const editor = new TextEditor();
const history = new HistoryManager();
console.log('=== Тестирование системы истории ===');
// Начальное состояние
editor.setContent('Привет, мир!');
editor.setCursorPosition(6);
history.saveMemento(editor.createMemento());
console.log('\n--- Изменения ---');
editor.setContent('Привет, JavaScript!');
editor.setCursorPosition(15);
editor.setSelection(7, 17);
history.saveMemento(editor.createMemento());
editor.setContent('Привет, TypeScript!');
editor.setCursorPosition(18);
history.saveMemento(editor.createMemento());
editor.setContent('Привет, React!');
editor.setCursorPosition(12);
history.saveMemento(editor.createMemento());
console.log('\n--- Текущее состояние ---');
editor.displayState();
console.log('\n--- Откат ---');
const undoMemento = history.undo();
if (undoMemento) {
editor.restoreMemento(undoMemento);
editor.displayState();
}
console.log('\n--- Еще один откат ---');
const undoMemento2 = history.undo();
if (undoMemento2) {
editor.restoreMemento(undoMemento2);
editor.displayState();
}
console.log('\n--- Повтор ---');
const redoMemento = history.redo();
if (redoMemento) {
editor.restoreMemento(redoMemento);
editor.displayState();
}
TypeScript с интерфейсами
// Интерфейс хранителя
interface Memento {
getState(): any;
getTimestamp(): string;
}
// Конкретный хранитель
class GameMemento implements Memento {
private state: any;
private timestamp: string;
constructor(state: any) {
this.state = { ...state }; // Глубокое копирование
this.timestamp = new Date().toISOString();
}
getState(): any {
return this.state;
}
getTimestamp(): string {
return this.timestamp;
}
}
// Создатель состояния - игра
class Game {
private level: number;
private score: number;
private lives: number;
private playerPosition: { x: number; y: number };
private inventory: string[];
constructor() {
this.level = 1;
this.score = 0;
this.lives = 3;
this.playerPosition = { x: 0, y: 0 };
this.inventory = [];
}
// Игровые действия
movePlayer(x: number, y: number): void {
this.playerPosition = { x, y };
console.log(`Игрок перемещен в позицию (${x}, ${y})`);
}
addScore(points: number): void {
this.score += points;
console.log(`Очки добавлены: +${points}. Общий счет: ${this.score}`);
}
loseLife(): void {
this.lives--;
console.log(`Потеряна жизнь. Осталось жизней: ${this.lives}`);
}
addToInventory(item: string): void {
this.inventory.push(item);
console.log(`Добавлено в инвентарь: ${item}`);
}
levelUp(): void {
this.level++;
console.log(`Уровень повышен до ${this.level}`);
}
// Создание снимка
createMemento(): Memento {
const state = {
level: this.level,
score: this.score,
lives: this.lives,
playerPosition: { ...this.playerPosition },
inventory: [...this.inventory]
};
return new GameMemento(state);
}
// Восстановление состояния
restoreMemento(memento: Memento): void {
const state = memento.getState();
this.level = state.level;
this.score = state.score;
this.lives = state.lives;
this.playerPosition = { ...state.playerPosition };
this.inventory = [...state.inventory];
console.log(`Состояние игры восстановлено из снимка от ${memento.getTimestamp()}`);
}
// Отображение состояния
displayState(): void {
console.log(`Уровень: ${this.level}, Счет: ${this.score}, Жизни: ${this.lives}`);
console.log(`Позиция: (${this.playerPosition.x}, ${this.playerPosition.y})`);
console.log(`Инвентарь: [${this.inventory.join(', ')}]`);
}
}
// Опекун - система сохранений
class SaveSystem {
private saves: Map<string, Memento> = new Map();
saveGame(saveName: string, memento: Memento): void {
this.saves.set(saveName, memento);
console.log(`Игра сохранена как "${saveName}"`);
}
loadGame(saveName: string): Memento | null {
const memento = this.saves.get(saveName);
if (memento) {
console.log(`Игра загружена из сохранения "${saveName}"`);
return memento;
} else {
console.log(`Сохранение "${saveName}" не найдено`);
return null;
}
}
getSaveNames(): string[] {
return Array.from(this.saves.keys());
}
deleteSave(saveName: string): void {
if (this.saves.delete(saveName)) {
console.log(`Сохранение "${saveName}" удалено`);
} else {
console.log(`Сохранение "${saveName}" не найдено`);
}
}
}
// Использование
const game = new Game();
const saveSystem = new SaveSystem();
console.log('=== Система сохранений игры ===');
// Играем
game.movePlayer(10, 5);
game.addScore(100);
game.addToInventory('меч');
game.addToInventory('щит');
console.log('\n--- Сохраняем игру ---');
saveSystem.saveGame('checkpoint1', game.createMemento());
// Продолжаем играть
game.movePlayer(20, 15);
game.addScore(200);
game.levelUp();
game.loseLife();
console.log('\n--- Сохраняем еще раз ---');
saveSystem.saveGame('checkpoint2', game.createMemento());
console.log('\n--- Текущее состояние ---');
game.displayState();
console.log('\n--- Загружаем первое сохранение ---');
const checkpoint1 = saveSystem.loadGame('checkpoint1');
if (checkpoint1) {
game.restoreMemento(checkpoint1);
game.displayState();
}
console.log('\n--- Загружаем второе сохранение ---');
const checkpoint2 = saveSystem.loadGame('checkpoint2');
if (checkpoint2) {
game.restoreMemento(checkpoint2);
game.displayState();
}
console.log('\n--- Список сохранений ---');
console.log('Доступные сохранения:', saveSystem.getSaveNames());
Когда использовать
Используйте Memento, когда:
- Нужно сохранять и восстанавливать состояние объекта
- Нельзя нарушать инкапсуляцию объекта
- Нужна система отмены/повтора операций
- Нужно создавать снимки состояния для отладки
Преимущества
- Не нарушает инкапсуляцию объекта
- Упрощает создание снимков состояния
- Позволяет реализовать отмену операций
- Централизует управление состоянием
Недостатки
- Может потреблять много памяти при частых снимках
- Сложно реализовать для объектов с циклическими ссылками
- Может быть избыточным для простых случаев
Отличия от других паттернов
- Command — инкапсулирует запросы как объекты
- State — изменяет поведение при изменении состояния
- Prototype — создает объекты путем клонирования
Реальный пример
// Memento для системы настроек приложения
class SettingsMemento {
constructor(settings) {
this.settings = JSON.parse(JSON.stringify(settings)); // Глубокое копирование
this.timestamp = new Date().toISOString();
}
getSettings() {
return this.settings;
}
getTimestamp() {
return this.timestamp;
}
}
class ApplicationSettings {
constructor() {
this.settings = {
theme: 'light',
language: 'ru',
fontSize: 14,
autoSave: true,
notifications: true,
darkMode: false
};
}
updateSetting(key, value) {
this.settings[key] = value;
console.log(`Настройка ${key} изменена на ${value}`);
}
getSettings() {
return this.settings;
}
createMemento() {
return new SettingsMemento(this.settings);
}
restoreMemento(memento) {
this.settings = memento.getSettings();
console.log(`Настройки восстановлены из снимка от ${memento.getTimestamp()}`);
}
displaySettings() {
console.log('Текущие настройки:', this.settings);
}
}
// Использование
const settings = new ApplicationSettings();
const settingsHistory = [];
console.log('=== Система истории настроек ===');
// Сохраняем начальные настройки
settingsHistory.push(settings.createMemento());
// Изменяем настройки
settings.updateSetting('theme', 'dark');
settings.updateSetting('fontSize', 16);
settingsHistory.push(settings.createMemento());
settings.updateSetting('language', 'en');
settings.updateSetting('notifications', false);
settingsHistory.push(settings.createMemento());
console.log('\n--- Текущие настройки ---');
settings.displaySettings();
console.log('\n--- Откат к предыдущим настройкам ---');
const previousMemento = settingsHistory[settingsHistory.length - 2];
settings.restoreMemento(previousMemento);
settings.displaySettings();
Memento — это сохранение и восстановление состояния!