28 вересня 2010 р.

"Патерн Вівторка" #8: Хранитель (Memento)

Якщо ви коли небуть бавилися в стрілялки, то дуже вірогідно, що ви знайомі із значенням хот-кеїв F5 та F9. I навіть якщо ви таки не мали шансу в житті погратися в "шпільки", ідея швидкого збереження поточного стану і відновлення до нього ідеологічно є знайомою (навіть якщо це було Ctrl+Z у програмі Word). Натискаючи F5 ви зберігаєте поточне місце знаходження і рівні життя/броні та, можливо, ще якусь інформацію, наприклад, скільки монстрів було вже вбито на даній позиції (напевно для того, щоб не "мочити" їх заново). Коли клавіша F9 натискається відбувається повернення до попереднього збереженого стану.

Під час збереження стану, швидше за все що вам мало хочеться щоб ця інформація була доступна іншими класами (інкапсуляція стану), таким чином ви будете певні, що ніхто не зменшить рівень життя. Також може знадобитися функціональність по збереженню послідовності станів. Наприклад можливість повернутися на 2 або 3 збереження назад натискаючи Shift+F9 (+F9).

Як це можна реалізувати?

ХРАНИТЕЛЬ

Патерн Хранитель (англ. Memento) використовується коли ви хочете відмінити операції без відображення внутрішньої структури Хазяїна (Originator - гра у нашому прикладі). Координація операці здійснюється Опікуном (Caretaker - контроллер гри), який надає можливість простого збереження миттєвих станів системи без уявлення що ці стани собою являють.

Давайте глянемо на реалізацію:

Originator (Game)

public class GameOriginator
{
    private GameState _state = new GameState(100, 0);//Health & Killed Monsters

    public void Play()
    {
        //During this Play method game's state is continuously changed
        Console.WriteLine(_state.ToString());
        _state = new GameState((int)(_state.Health * 0.9), _state.KilledMonsters + 2);
    }

    public GameMemento GameSave()
    {
        return new GameMemento(_state);
    }

    public void LoadGame(GameMemento memento)
    {
        _state = memento.GetState();
    }
}

GameMemento

public class GameMemento
{
    private readonly GameState _state;

    public GameMemento(GameState state)
    {
        _state = state;
    }

    public GameState GetState()
    {
        return _state;
    }
}

Таким чином цей клас може згенерувати інстанс хранителя із поточним знімком (станом) гри, в той же час можна витягнути стан із уже існуючого хранителя, але при цьому ніхто більше не буде працювати із станом гри напряму.

Caretaker

Або той хто відповідає за збереження ваших F5/F9. Нижче наведений код може тільки завантажити останній збережений "сейв", але все можна легко вдосконалити.

public class Caretaker
{
    private readonly GameOriginator _game = new GameOriginator();
    private readonly Stack< GameMemento > _quickSaves = new Stack< GameMemento >();

    public void ShootThatDumbAss()
    {
        _game.Play();
    }

    public void F5()
    {
        _quickSaves.Push(_game.GameSave());
    }

    public void F9()
    {
        _game.LoadGame(_quickSaves.Peek());
    }
}

Вивід

Припустимо що гра протікала так:

var caretaker = new Caretaker();
caretaker.F5();
caretaker.ShootThatDumbAss();
caretaker.F5();
caretaker.ShootThatDumbAss();
caretaker.ShootThatDumbAss();
caretaker.ShootThatDumbAss();
caretaker.ShootThatDumbAss();
caretaker.F9();
caretaker.ShootThatDumbAss();

В такому випадку наша демонстраційна програма згенерує такий вивід (State має перевантажений метод ToString):

Health: 100
Killed Monsters: 0
Health: 90
Killed Monsters: 2
Health: 81
Killed Monsters: 4
Health: 72
Killed Monsters: 6
Health: 64
Killed Monsters: 8
Health: 90
Killed Monsters: 2


А ось проста UML діаграма, намальована для цього прикладу:

3 коментарі: