Паттерн Хранитель (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 дает нам следующие преимущества:
- Уменьшение связанности системы
- Сохранение инкапсуляции информации
- Определение простого интерфейса для сохранения и восстановления состояния
В то же время мы можем столкнуться с недостатками, в частности, если требуется сохранение большого объема информации, то возрастут издержки на хранение всего объема состояния.