«Хранитель» (Memento), также известный как Снимок – поведенческий паттерн проектирования. Он позволяет определять, сохранять, а также восстанавливать предыдущие состояния объектов без нарушения принципа инкапсуляции.
Самый простой и наглядный пример использования этого паттерна – некий текстовый редактор, который позволяет изменять форматирование текста и других элементов. Но при этом пользователь может эти изменения отменить. Другой пример – восстановление состояния персонажей в игре на контрольных точках.
Формально, в виде диаграмм структуру паттерна можно представить так:
Участники процесса:
- Memento («хранитель») – хранитель, сохраняет состояние объекта Originator;
- Originator («создатель») – создает экземпляр объекта хранителя. Имеет полный доступ к Memento;
- Caretaker («опекун») – производит сохранения состояний.
Теперь рассмотрим очень упрощённый пример текстового редактора. У него будет всего пять команд:
- добавление нового блока текста;
- установка стиля текста;
- вывод всего текста на экран;
- сохранение текущего состояния документа;
- отмена последнего действия по редактированию документа.
Для начала создадим класс нашего документа – класс Doc. Он будет содержать в себе два параметра – текст и стиль, которые пользователь может изменить с помощью соответствующих методов – AddBlock(string text) и SetStyle(int style). Выводить содержимое будем через метод Print().
class Doc
{
private string text = ""; // текст документа
private int style = 1; // стиль всего текста
public void AddBlock(string text)
{
this.text += text + '\n';
Console.WriteLine("Добавлен блок:\n{0}", text);
}
public void SetStyle(int style)
{
if (this.style != style)
{
this.style = style;
}
Console.WriteLine("Установлен стиль: тип {0}", style);
}
public void Print()
{
Console.WriteLine("\n--- ПЕЧАТЬ ---\nСтиль: тип {1}\nТекст:\n{0}", text, style);
}
}
Далее нам нужно создать класс для хранения состояния документа – DocMemento.
class DocMemento
{
public string Text { get; private set; }
public int Style { get; private set; }
public DocMemento(string text, int style)
{
Text = text;
Style = style;
}
}
В данном случае это своего рода контейнер-копия сохранённого состояния объекта Doc. Мы передаём не копию экземпляра документа, а только его состояние со значимыми параметрами.
Теперь снова вернёмся к классу Doc и добавим два метода: для сохранения в объект-memento и восстановления состояния из объекта-memento:
public DocMemento SaveState()
{
Console.WriteLine("Сохранение документа.");
return new DocMemento(text, style);
}
public void RestoreState(DocMemento memento)
{
text = memento.Text;
style = memento.Style;
}
Создадим класс, который будет в себе содержать историю изменений документа – EditorHistory. Вся история состояний будет храниться в стеке, который будет скрыт от пользователя для доступа напрямую.
class EditorHistory
{
private Stack History { get; set; }
public EditorHistory()
{
History = new Stack();
}
public void Push(DocMemento memento)
{
Console.WriteLine("Сохранение документа.");
History.Push(memento);
}
public DocMemento Pop()
{
Console.WriteLine("Отмена последних действий.");
return History.Pop();
}
}
Всё готово, теперь можно создать класс редактора и наполнить пользовательскими действиями:
using System;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
TextEditor.Run();
}
}
class TextEditor
{
public static void Run()
{
Doc myDocument = new Doc();
EditorHistory history = new EditorHistory();
myDocument.AddBlock("Привет, мир!");
myDocument.SetStyle(2);
myDocument.Print();
history.Push(myDocument.SaveState());
myDocument.AddBlock("И снова привет!!!");
myDocument.SetStyle(3);
myDocument.Print();
myDocument.RestoreState(history.Pop());
myDocument.Print();
Console.Read();
}
}
Для примера мы сделали сохранение состояния после ввода блока текста «Привет, мир!» и смены параметра стиля текста. Сохранили в объекте history, снова изменили документ и вернули прежнее состояние. Каждое изменение сопроводили выводом всего документа на экран, то есть в консоль.
Если сравнивать с представленной в самом начале схемой, то в роли Originator у нас выступает Doc, Memento – DocMemento, а в роли Caretaker – EditorHistory. Документу доступны все поля, поэтому именно он делает снимок. А из истории берёт состояние для восстановления.
Данный пример слишком простой. Добавляя новые структуры и объекты в наш редактор, мы будем усложнять состояние документа (добавятся значения для разных текстовых блоков, страниц и абзацев, геометрические объекты, рисунки и тому подобное). Поэтому для более удобного представления состояния документа нам придётся использовать отдельные классы контейнеров данных, которые будут содержать множество полей. Таким образом, в более сложных программах для хранения состояний может потребоваться много памяти, если снимков будет много – это, пожалуй, основной недостаток паттерна.
Есть и чуть более сложные вариации реализации данного паттерна – создавая пустой промежуточный интерфейс или же более широкий вариант, с возможностью иметь множество видов создателей и снимков. Последний, например, позволяет полностью исключить доступ к состоянию создателей и снимков, но при этом сам опекун становится независимым от создателей.
Очень часто паттерн «Хранитель» совместно используется с паттерном «Команда» (как раз для выполнения команд «Сохранить» и «Восстановить»).
Итого, «Хранитель» позволяет нам передавать сохраняемые состояния объекту, но не передавать ему управление самим сохраняемым объектом, сохраняя инкапсуляцию.