В этой статье будет рассмотрен паттерн проектирования хранитель C# — Memento C#, для чего он нужен и какие проблемы он решает, где можно применять данный шаблон и когда это будет излишним.
Паттерн проектирования — это продуманный способ построения исходного кода программы для решения часто возникающих в повседневном программировании проблем проектирования. Иными словами, это уже придуманное решения, для типичной задачи. При этом паттерн не готовое решение, а просто алгоритм действий, который должен привести к желаемому результату. Давайте рассмотрим один из наиболее часто используемых поведенческих паттернов — Хранитель (Memento).
Подпишись на группу Вконтакте и Телеграм-канал. Там еще больше полезного контента для программистов.
А на YouTube-канале ты найдешь обучающие видео по программированию. Подписывайся!
Как я уже писал ранее, существует три вида паттернов проектирования:
- Порождающие паттерны позволяют возможность выполнять инициализацию объектов наиболее удобным и оптимальным способом.
- Структурные паттерны описывают взаимоотношения между различными классами или объектами, позволяя им совместно реализовывать поставленную задачу.
- Поведенческие паттерны позволяют грамотно организовать связь между сущностями для оптимизации и упрощения их взаимодействия.
Хранитель (Memento) — это поведенческий паттерн, который позволяет сохранить состояние состояние экземпляра объекта не раскрывая его полную внутреннюю структуру. То есть, данный паттерн позволяет сделать снимок объекта с возможностью восстановления состояния объекта из этого снимка, при этом не нарушая принцип инкапсуляции.
Давайте рассмотрим диаграмму паттерна Хранитель

- Originator — основной объект. Именно его состояние будет сохраняться и восстанавливаться
- Memento — снимок состояния основного объекта. Хранит в тебе все важные данные
- Carataker — хранилище снимков, позволяет сохранять и восстанавливать любое из хранимых состояний.
Давайте рассмотрим основную логику работы паттерна Хранитель. Если у нас есть объект, который с которым ведется основное взаимодействие и нам нужно сохранить его состояние, но при этом мы не хотим делать его полную копию, так как в некоторых случаях это может раскрыть его внутреннюю структуру или предоставить доступ к свойствам и методам нежелательным пользователям, то мы можем создать уменьшенную копию объекта только с теми данными, которые хотим сохранить. Это позволит не только защитится от нежелательного доступа и соблюсти инкапсуляцию, но и также экономит ресурсы при хранении больших объемов данных. Первое, что приходит в голову при рассмотрении данного паттерна, это конечно же сохранение в компьютерных играх. Вспомним другую статью, где я уже использовал близкую предметную область. Это была статья Паттерн проектирования Абстрактная фабрика (Abstract Factory) на C#. В ней рассматривались космические корабли, позволяющие соревноваться в скорости и в битве. Переработаем данную предметную область и реализуем сохранение состояние космического корабля перед боем с возможностью загрузиться.
Реализация
Для начала реализуем класс космического корабля. В нем мы реализуем необходимые свойства и методы для работы корабля, а так же методы сохранения состояния и восстановления состояния из снимка. Обратите внимание, что мы не раскрываем внутреннюю структуру класса, так как именно внутри класса определяем логику сохранения и загрузки. Поэтому я специально добавил свойство RandomValue, которое не попадает в снимок, чтобы показать что класс сам контролирует какие свойства являются для него важными. Так же взаимодействие с классом MementoSpaceship осуществляется с через набор параметров-свойств, а не целым объектом класса Spaceship. Если бы в MementoSpaceship передавался экземпляр класса Spaceship, то было бы чрезмерное раскрытие внутренней структуры класса, снимок получил бы доступ ко всем свойствам и даже методам класса, и мог бы внести какие-либо изменения. В данном случае же мы от этого застрахованы. Это позволяет лучше придерживаться принципа инкапсуляции.
using System; namespace Memento { /// <summary> /// Космический корабль. /// </summary> public class Spaceship { /// <summary> /// Название космического корабля. /// </summary> public string Name { get; private set; } /// <summary> /// Запас здоровья космического корабля. /// </summary> public int Health { get; private set; } /// <summary> /// Случайное значение. /// </summary> public int RandomValue { get; private set; } /// <summary> /// Создать экземпляр космического корабля. /// </summary> /// <param name="name"> Название корабля. </param> /// <param name="health"> Уровень здоровья корабля. </param> public Spaceship(string name, int health = 100) { // Проверяем входные данные на корректность. if(string.IsNullOrEmpty(name)) { throw new ArgumentNullException(nameof(name)); } if (health <= 0) { throw new ArgumentException("Здоровье космического корабля не может быть меньше либо равно нулю.", nameof(health)); } // Устанавливаем свойства. Name = name; Health = health; var rnd = new Random(); RandomValue = rnd.Next(); } /// <summary> /// Выстрелить. /// </summary> /// <returns> Нанесенный урон. </returns> public int Shoot() { // Задаем минимальный и максимальный возможный урон. const int MinDamage = 0; const int MaxDamage = 10; // Вычисляем значение нанесенного урона. var rnd = new Random(); var damage = rnd.Next(MinDamage, MaxDamage); return damage; } /// <summary> /// Получить урон. /// </summary> /// <param name="damage"> Величина полученного урона. </param> public void TakeDamage(int damage) { // Проверяем входные данные на корректность. if(damage < 0) { throw new ArgumentException("Нанесенный урон не может быть меньше нуля", nameof(damage)); } // Изменяем значение здоровья. Health -= damage; } /// <summary> /// Сохранить состояние космического корабля. /// </summary> /// <returns> Состояние корабля. </returns> public MementoSpaceship Save() { // Формируем снимок для сохранения состояние. var memento = new MementoSpaceship(Name, Health); return memento; } /// <summary> /// Восстановить состояние космического корабля из снимка. /// </summary> /// <param name="memento"> Снимок состояния космического корабля. </param> public void Restore(MementoSpaceship memento) { // Проверяем входные данные на корректность. if(memento == null) { throw new ArgumentNullException(nameof(memento)); } if(string.IsNullOrEmpty(memento.SpaceshipName)) { throw new ArgumentNullException(nameof(memento.SpaceshipName)); } if(memento.Health <= 0) { throw new ArgumentException("Здоровье космического корабля не может быть меньше либо равно нулю.", nameof(memento.Health)); } // Восстанавливаем состояние из снимка. Name = memento.SpaceshipName; Health = memento.Health; } /// <summary> /// Приведение объекта к строке. /// </summary> /// <returns> Название космического корабля. </returns> public override string ToString() { return Name; } } }
Так же реализуем класс контроллер для выполнения сражений между кораблями.
using System; namespace Memento { /// <summary> /// Класс-контроллер отвечающий за проведение боев между космическими кораблями. /// </summary> public class Battle { /// <summary> /// Первый космический корабль. /// </summary> private Spaceship _ship1; /// <summary> /// Второй космический корабль. /// </summary> private Spaceship _ship2; /// <summary> /// Создать экземпляр битвы. /// </summary> /// <param name="ship1"> Первый космический корабль. </param> /// <param name="ship2"> Второй космический корабль. </param> public Battle(Spaceship ship1, Spaceship ship2) { _ship1 = ship1 ?? throw new ArgumentNullException("Первый космический корабль не может быть пустым.", nameof(ship1)); _ship2 = ship2 ?? throw new ArgumentNullException("Второй космический корабль не может быть пустым.", nameof(ship2)); } /// <summary> /// Бой между космическими кораблями. /// </summary> /// <returns> Победивший в бою корабль. null - если оба корабля погибли. </returns> public Spaceship Fight() { // Сражаемся насмерть, пока у одного из космических кораблей не закончится здоровье. while (_ship1.Health > 0 && _ship2.Health > 0) { _ship2.TakeDamage(_ship1.Shoot()); _ship1.TakeDamage(_ship2.Shoot()); } // Выявляем победителя if (_ship1.Health > 0) { return _ship1; } if (_ship2.Health > 0) { return _ship2; } return null; } } }
Теперь создадим класс хранитель. В нем нужно указать все поля, которые должны быть сохранены, и при необходимости добавить вспомогательные, как например дата создания снимка.
using System; namespace Memento { /// <summary> /// Снимок состояния космического корабля. /// </summary> public class MementoSpaceship : ICloneable { /// <summary> /// Дата и время создания снимка. /// </summary> public DateTime CreatedAt { get; private set; } /// <summary> /// Название космического корабля. /// </summary> public string SpaceshipName { get; private set; } /// <summary> /// Здоровье космического корабля. /// </summary> public int Health { get; private set; } /// <summary> /// Создать новый экземпляр снимка космического корабля. /// </summary> /// <param name="spaceshipName"> Название. </param> /// <param name="health"> Состояние здоровья. </param> public MementoSpaceship(string spaceshipName, int health) { // Проверяем входные параметры на корректность. if(string.IsNullOrEmpty(spaceshipName)) { throw new ArgumentNullException(nameof(spaceshipName)); } if(health <= 0) { throw new ArgumentException("Здоровье космического корабля не может быть меньше или равным нулю.", nameof(health)); } // Устанавливаем значения. SpaceshipName = spaceshipName; Health = health; CreatedAt = DateTime.UtcNow; } /// <summary> /// Создать копию объекта. /// </summary> /// <returns> Копия объекта. </returns> public object Clone() { return MemberwiseClone(); } /// <summary> /// Приведение объекта к строке. /// </summary> /// <returns> Дата и время создания снимка. </returns> public override string ToString() { return CreatedAt.ToString(); } } }
Ну и наконец добавим класс для хранения снимков. Он позволяет добавлять снимок в коллекцию всех состояний, а так же получать последний снимок. При необходимости можно реализовывать дополнительную логику работы, например получение конкретного снимка.
using System; using System.Collections.Generic; using System.Linq; namespace Memento { /// <summary> /// Хранилище состояний космического корабля. /// </summary> public class CaratakerSpaceship { /// <summary> /// Коллекция всех снимков состояния космического корабля. /// </summary> private List<MementoSpaceship> _mementos = new List<MementoSpaceship>(); /// <summary> /// Сохранения состояний космического корабля. /// </summary> public IReadOnlyList<MementoSpaceship> Mementos => _mementos; /// <summary> /// Создать новый экземпляр хранилища состояний космического корабля. /// </summary> public CaratakerSpaceship() { } /// <summary> /// Сохранить снимок в хранилище. /// </summary> /// <param name="memento"> Снимок состояние космического корабля. </param> public void Save(MementoSpaceship memento) { // Проверяем входные данные на корректность. if(memento == null) { throw new ArgumentNullException(nameof(memento)); } // Добавляем состояние в коллекцию. _mementos.Add(memento); } /// <summary> /// Получить последний снимок состояния космического корабля. /// </summary> /// <returns> Снимок состояния космического корабля. </returns> public MementoSpaceship Restore() { if(_mementos.Count == 0) { throw new IndexOutOfRangeException("Не возможно восстановить состояние из коллекции. Коллекция пуста."); } var result = _mementos.LastOrDefault() ?? throw new IndexOutOfRangeException("Не возможно восстановить состояние из коллекции. Не удалось получить состояние."); return result.Clone() as MementoSpaceship; } } }
Теперь нам остается только реализовать взаимодействие с пользователем и поработать со снимками.
using System; namespace Memento { class Program { static void Main(string[] args) { Console.WriteLine("Добро пожаловать на 77-ые Космические игры! Представьтесь..."); // Запрашиваем имя пользователя, пока он ни введет не пустую строку. var name = ""; while (true) { name = Console.ReadLine(); if (string.IsNullOrEmpty(name)) { Console.WriteLine("Введите Ваше имя!"); } else { break; } } // Создаем хранилище состояний. var carataker = new CaratakerSpaceship(); // Счетчик побед. var winCounter = 0; // Создаем космический корабль игрока. var playerShip = new Spaceship(name, 300); // Повторяем пока пользователь не выйдет. while (true) { // Создаем космический корабль противника. var computerShip = new Spaceship($"Гладиатор №{winCounter + 1}"); // Представляем участников и создаем объект сражения с этими участниками. Console.WriteLine($"Первый претендент: {playerShip}. Уровень здоровья {playerShip.Health}."); Console.WriteLine($"Второй претендент: {computerShip}. Уровень здоровья {computerShip.Health}"); var battle = new Battle(playerShip, computerShip); // Запрашиваем у пользователя продолжение. Console.WriteLine("Да начнется смертельная битва! Продолжить?(y/n)"); var ansver = Console.ReadLine(); if (ansver.ToLower() == "y") { // Если пользователь согласился, то начинаем битву. var battleWinner = battle.Fight(); if (battleWinner == playerShip) { // Если выиграл пользователь, то поздравляем его, сохраняем новый снимок и переходим к следующему туру. Console.WriteLine($"Поздравляем! Вы выиграли в битве, {battleWinner}"); // Формируем снимок корабля и сохраняем снимок в хранилище снимков. var saveMemento = playerShip.Save(); carataker.Save(saveMemento); // Увеличиваем количество побед. winCounter++; continue; } else { // Если выиграл компьютер, то предлагаем загрузиться из последнего сохранения. Console.WriteLine("Сожалеем, но вы погибли в сражении. Загрузить последнее сохранение?(y/n)"); var answer2 = Console.ReadLine(); if (ansver.ToLower() == "y") { // Если пользователь согласился, то загружаем игру и начинаем раунд заново. // Загружаем состояние. var loadMemento = carataker.Restore(); playerShip.Restore(loadMemento); // Сообщаем пользователю. Console.WriteLine("Игра загружена. Нажмите ввод, чтобы продолжить."); Console.ReadLine(); continue; } else if(ansver.ToLower() == "n") { // Иначе если пользователь отказался загружаться, то сообщаем количество побед. Console.WriteLine($"Вы доблестно победили в {winCounter} битвах и погибли с честью."); Console.ReadLine(); break; } } } else if (ansver.ToLower() == "n") { // Иначе если пользователь отказался сражаться, то сообщаем ему результат. if (winCounter == 0) { Console.WriteLine("Вы с позором отказались от битвы, но остались целы и невредимы."); } else { Console.WriteLine($"Поздравляем! Вы с доблестью выиграли {winCounter} сражений и уходите победителем."); } break; } // Был введен некорректный ответ. Console.WriteLine("Некорректный ввод ответа. " + "Если хотите продолжить, введите английский символ y, " + "если хотите отказаться, введите английский символ n."); } // Игра завершена. Console.WriteLine("Игра завершена."); Console.ReadLine(); } } }
В итоге получаем следующий результат.

Исходный код доступен в репозитории github.
Кроме того, рекомендую прочитать статью C# Array | Работа с массивами в C#. А также подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.