Паттерн Хранитель (Memento) позволяет выносить внутреннее состояние объекта за его пределы для последующего возможного восстановления объекта без нарушения принципа инкапсуляции.
Когда использовать Memento?
Когда нужно сохранить состояние объекта для возможного последующего восстановления
Когда сохранение состояния должно проходить без нарушения принципа инкапсуляции
То есть ключевыми понятиями для данного паттерна являются сохранение внутреннего состояния и инкапсуляция, и важно соблюсти баланс между ними. Ведь, как правило, если мы не нарушаем инкапсуляцию, то состояние объекта хранится в объекте в приватных переменных. И не всегда для доступа к этим переменным есть методы или свойства с сеттерами и геттерами. Например, в игре происходит управление героем, все состояние которого заключено в нем самом - оружие героя, показатель жизней, силы, какие-то другие показатели. И нередко может возникнуть ситуация, сохранить все эти показатели во вне, чтобы в будущем можно было откатиться к предыдущему уровню и начать игру заново. В этом случае как раз и может помочь паттерн Хранитель.
С помощью диаграмм структуру паттерна можно изобразить следующим образом:
Формальная структура паттерна на языке C#:
class Memento { public string State { get; private set;} public Memento(string state) { this.State = state; } } class Caretaker { public Memento Memento { get; set; } } class Originator { public string State { get; set; } public void SetMemento(Memento memento) { State = memento.State; } public Memento CreateMemento() { return new Memento(State); } }
Memento: хранитель, который сохраняет состояние объекта Originator и предоставляет полный доступ только этому объекту Originator
Originator: создает объект хранителя для сохранения своего состояния
Caretaker: выполняет только функцию хранения объекта Memento, в то же время у него нет полного доступа к хранителю и никаких других операций над хранителем, кроме собственно сохранения, он не производит
Теперь рассмотрим реальный пример: нам надо сохранять состояние игрового персонажа в игре:
class Program { static void Main(string[] args) { Hero hero = new Hero(); hero.Shoot(); // делаем выстрел, осталось 9 патронов GameHistory game = new GameHistory(); game.History.Push(hero.SaveState()); // сохраняем игру hero.Shoot(); //делаем выстрел, осталось 8 патронов hero.RestoreState(game.History.Pop()); hero.Shoot(); //делаем выстрел, осталось 8 патронов Console.Read(); } } // Originator class Hero { private int patrons=10; // кол-во патронов private int lives=5; // кол-во жизней public void Shoot() { if (patrons > 0) { patrons--; Console.WriteLine("Производим выстрел. Осталось {0} патронов", patrons); } else Console.WriteLine("Патронов больше нет"); } // сохранение состояния public HeroMemento SaveState() { Console.WriteLine("Сохранение игры. Параметры: {0} патронов, {1} жизней", patrons, lives); return new HeroMemento(patrons, lives); } // восстановление состояния public void RestoreState(HeroMemento memento) { this.patrons=memento.Patrons; this.lives = memento.Lives; Console.WriteLine("Восстановление игры. Параметры: {0} патронов, {1} жизней", patrons, lives); } } // Memento class HeroMemento { public int Patrons { get; private set; } public int Lives { get; private set; } public HeroMemento(int patrons, int lives) { this.Patrons = patrons; this.Lives = lives; } } // Caretaker class GameHistory { public Stack<HeroMemento> History { get; private set; } public GameHistory() { History = new Stack<HeroMemento>(); } }
Консольный вывод программы:
Производим выстрел. Осталось 9 патронов Сохранение игры. Параметры: 9 патронов, 5 жизней Производим выстрел. Осталось 8 патронов Восстановление игры. Параметры: 9 патронов, 5 жизней Производим выстрел. Осталось 8 патронов
Здесь в роли Originator выступает класс Hero, состояние которого описывается количество патронов и жизней. Для хранения
состояния игрового персонажа предназначен класс HeroMemento. С помощью метода SaveState()
объект Hero может
сохранить свое состояние в HeroMemento, а с помощью метода RestoreState()
- восстановить.
Для хранения состояний предназначен класс GameHistory, причем все состояния хранятся в стеке, что позволяет с легкостью извлекать последнее сохраненное состояние.
Использование паттерна Memento дает нам следующие преимущества:
Уменьшение связанности системы
Сохранение инкапсуляции информации
Определение простого интерфейса для сохранения и восстановления состояния
В то же время мы можем столкнуться с недостатками, в частности, если требуется сохранение большого объема информации, то возрастут издержки на хранение всего объема состояния.