Pull to refresh

Паттерн проектирования “Хранитель”/ Memento

В этом посте я расскажу вам о паттерне проектирования «Хранитель». Посмотрим с чем его едят, и что он из себя представляет.

Почитать описание других паттернов.

Проблема


Необходимо сохранять, фиксировать и восстанавливать внутреннее состояние объекта не нарушая инкапсуляцию.

Описание


Представим, что у нас есть объект, у которого есть внутреннее состояние в виде закрытого поля. От его значения зависит всё поведение объекта, вся его логика строится от этого поля, своеобразная жизненная ситуация, от которой объект выбирает своё поведение. Значение этого поля формируется внутренними методами объекта, реализующие хитрую логику. Инкапсуляция говорит нам, что это сердце системы надо защитить, сделав его скрытым(private) от глаз системы. Но представим, что нам необходимо реализовать сохранение состояния этого объекта, что бы в любой момент времени можно было восстановить сохраненное состояние. Сделать внутренне поле открытым, сохранить его во внешнюю переменную, а в нужный момент восстановить? Тогда мы нарушим инкапсуляцию.

Один из способов решения данной задачи — использование паттерна ” Хранитель ”.

Паттерн Хранитель (также известный как Memento, Token, Лексема) позволяет не нарушая инкапсуляцию зафиксировать и сохранить внутреннее состояния объекта так, чтобы позднее восстановить его в этом состоянии.


Шаблон Хранитель используется, когда:
— необходимо сохранить снимок состояния объекта (или его части) для последующего восстановления
— прямой интерфейс получения состояния объекта раскрывает детали реализации и нарушает инкапсуляцию объекта

И так разберем, что же из себя представляет паттерн Хранитель. У нас есть главный объект, который имеет внутреннее состояние, именуемый ”Создатель” (Originator). Так же у нас есть объект, который может выполнять определенные действия над “Создателем” (при этом внутреннее состояние “Создателя” может изменяться) и имеет возможность сохранять и восстанавливать состояние “Создателя”, когда ему это необходимо. Этот класс именуется “Опекун”(Caretaker). Для того, что бы “Опекун” смог запомнить внутреннее состояние “Создателя”, он запрашивает у него объект, содержащий необходимые данные, именуемый “Хранитель” (Memento). И когда “Опекуну” необходимо будет откатить состояние “Создателя”, он передает ему обратно объект “Хранитель”. Получив его, “Создатель” восстанавливает своё внутреннее состояние исходя из данных содержащихся в “Хранителе”. Благодаря этому поле, определяющее внутреннее состояние остается закрытым.

image

UML диаграмма, описывающая шаблон Хранитель

Практика


Давайте разберем этот паттерн на практике. Для примера мне вспомнилась игра Resident Evil (части 1, 2, 3) для консоли Play Station 1. Мне там очень понравилось, как реализована система здоровья. В игре не показывался процент здоровья, а только индикатор пульса. Если индикатор зеленый — то все хорошо, если желтый – среднее состояние, если красный – очень опасно для жизни. И в зависимости от этого главный герой либо идет ровно, либо придерживается рукой за живот и медленней передвигается, либо еле плетется. Представим, что мы разработчики этой игры и реализуем систему быстрых сохранений и загрузок состояния здоровья персонажа.
И так у нас будет 3 класса: Player(класс игрока, выступает в роли ”Создателя”), GameTools(класс в котором будет реализовано быстрое сохранение и загрузка состояния игрока, выступает в роли ”Опекуна”) и класс Memento(который будет выступать в роли ”Хранителя”). Так как процент состояния здоровья скрыт от нас, то он и будет исполнять роль внутреннего состояния объекта Player.

Напишем интерфейс, реализующий логику объекта Originator шаблона Хранитель.

internal interface IOriginator
  {
    Memento GetMemento();
    void SetMemento(Memento memento);
  }

* This source code was highlighted with Source Code Highlighter.

Теперь напишем класс игрока, реализующего логику интерфейса IOriginator

class Player: IOriginator
  {    
    private int _helth;           

    public Player()
    {
      _helth = 100;      
    }
    
    public void GetHurt(int hurt)
    {
      _helth -= hurt;    
    }

    public void GetCure(int cure)
    {
      _helth += cure;      
    }

    public void PrintPulse()
    {
      if(_helth > 70)
        Console.WriteLine("Green");

      if(_helth <= 70 && _helth > 40)
        Console.WriteLine("Yellow");

      if (_helth <= 40)
        Console.WriteLine("Red");
    }

    public void SetMemento(Memento memento)
    {
      _helth = memento.GetState();
    }

    public Memento GetMemento()
    {
      return new Memento(_helth);
    }
  }


* This source code was highlighted with Source Code Highlighter.

Здесь поле _helth выполняет роль внутреннего состояния. От него зависит поведение метода PrintPulse и оно полностью скрыто для внешней среды. Метод SetMemento устанавливает внутреннее состояние, которое извлекается из “Хранителя”. Метод GetMemento возвращает объект ”Хранителя”.

Теперь реализуем сам объект Memento.

internal class Memento
  {
    private int _helth;

    public Memento(int helth)
    {
      _helth = helth;
    }

    public int GetState()
    {
      return _helth;
    }
  }


* This source code was highlighted with Source Code Highlighter.

Как видно, метод GetState как раз и возвращает внутреннее состояние, указанное при создании объекта.

Класс GameUtils выглядит следующим образом:

class GameUtils
  {
    private Memento _memento;

    public void SaveState(IOriginator originator)
    {
      if (originator == null)
        throw new ArgumentNullException("originator is null");
      
      _memento = originator.GetMemento();

      Console.WriteLine("Save state");
    }

    public void LoadState(IOriginator originator)
    {
      if (originator == null)
        throw new ArgumentNullException("originator is null");
      if (_memento == null)
        throw new ArgumentNullException("memento is null");
      
      originator.SetMemento(_memento);

      Console.WriteLine("Load State");
    }
  }


* This source code was highlighted with Source Code Highlighter.

Теперь попробуем всё это дело на практике:

    static void Main(string[] args)
    {
      GameUtils gameUtils = new GameUtils();     
      Player player = new Player();
      

      player.GetHurt(20); //нанесено урон 20
      player.GetHurt(30); //нанесено урон 30
      player.GetHurt(20); //нанесено урон 20
      player.PrintPulse();//печатаем пульс

      //сохраняем состояние
      gameUtils.SaveState(player);

      player.GetCure(30); //принимем лекарство
      player.PrintPulse();//печатаем пульс

      //восстанавливаем состояние
      gameUtils.LoadState(player);
      
      player.PrintPulse(); //печатаем пульс

      Console.ReadLine();
    }


* This source code was highlighted with Source Code Highlighter.

Результат:
Red
Save state
Yellow
Load State
Red

На этом всё.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.