Карта дня, май, перетяжка
Карта дня, май, перетяжка
Карта дня, май, перетяжка

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

Аватарка пользователя Мария Кривоченко
Отредактировано

Обсудим паттерн ООП проектирования Хранитель на примере текстового редактора, который меняет форматирование текста и других элементов

11К открытий13К показов

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

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

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

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

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

Следите за новыми постами
Следите за новыми постами по любимым темам
11К открытий13К показов
OSZAR »