Обложка: Паттерн ООП «Хранитель»

Паттерн ООП «Хранитель»

Александр Саваткин
Александр Саваткин

Lead developer в компании Alawar

«Хранитель» (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. Документу доступны все поля, поэтому именно он делает снимок. А из истории берёт состояние для восстановления.

Данный пример слишком простой. Добавляя новые структуры и объекты в наш редактор, мы будем усложнять состояние документа (добавятся значения для разных текстовых блоков, страниц и абзацев, геометрические объекты, рисунки и тому подобное). Поэтому для более удобного представления состояния документа нам придётся использовать отдельные классы контейнеров данных, которые будут содержать множество полей. Таким образом, в более сложных программах для хранения состояний может потребоваться много памяти, если снимков будет много – это, пожалуй, основной недостаток паттерна.

Есть и чуть более сложные вариации реализации данного паттерна – создавая пустой промежуточный интерфейс или же более широкий вариант, с возможностью иметь множество видов создателей и снимков. Последний, например, позволяет полностью исключить доступ к состоянию создателей и снимков, но при этом сам опекун становится независимым от создателей.

Очень часто паттерн «Хранитель» совместно используется с паттерном «Команда» (как раз для выполнения команд «Сохранить» и «Восстановить»).

Итого, «Хранитель» позволяет нам передавать сохраняемые состояния объекту, но не передавать ему управление самим сохраняемым объектом, сохраняя инкапсуляцию.