Команда (Command)

Общая инфа

Паттерн «Команда» (Command) позволяет инкапсулировать запрос на выполнение определенного действия в виде отдельного объекта. Этот объект запроса на действие и называется командой. При этом объекты, инициирующие запросы на выполнение действия, отделяются от объектов, которые выполняют это действие.

Команды могут использовать параметры, которые передают ассоциированную с командой информацию. Кроме того, команды могут ставиться в очередь и также могут быть отменены.

Когда использовать команды?

Когда надо передавать в качестве параметров определенные действия, вызываемые в ответ на другие действия. То есть когда необходимы функции обратного действия в ответ на определенные действия.

Когда необходимо обеспечить выполнение очереди запросов, а также их возможную отмену.

Когда надо поддерживать логгирование изменений в результате запросов. Использование логов может помочь восстановить состояние системы — для этого необходимо будет использовать последовательность запротоколированных команд.

Схематично в UML паттерн Команда представляется следующим образом:

Паттерн Команда в C#

Формальное определение на языке C# может выглядеть следующим образом:

abstract class Command
{
    public abstract void Execute();
    public abstract void Undo();
}
// конкретная команда
class ConcreteCommand : Command
{
    Receiver receiver;
    public ConcreteCommand(Receiver r)
    {
        receiver = r;
    }
    public override void Execute()
    {
        receiver.Operation();
    }
 
    public override void Undo()
    {}
}
 
// получатель команды
class Receiver
{
    public void Operation()
    { }
}
// инициатор команды
class Invoker
{
    Command command;
    public void SetCommand(Command c)
    {
        command = c;
    }
    public void Run()
    {
        command.Execute();
    }
    public void Cancel()
    {
        command.Undo();
    }
}
class Client
{  
    void Main()
    {
        Invoker invoker = new Invoker();
        Receiver receiver = new Receiver();
        ConcreteCommand command=new ConcreteCommand(receiver);
        invoker.SetCommand(command);
        invoker.Run();
    }
}

Участники

  • Command: интерфейс, представляющий команду. Обычно определяет метод Execute() для выполнения действия, а также нередко включает метод Undo(), реализация которого должна заключаться в отмене действия команды
  • ConcreteCommand: конкретная реализация команды, реализует метод Execute(), в котором вызывается определенный метод, определенный в классе Receiver
  • Receiver: получатель команды. Определяет действия, которые должны выполняться в результате запроса.
  • Invoker: инициатор команды — вызывает команду для выполнения определенного запроса
  • Client: клиент — создает команду и устанавливает ее получателя с помощью метода SetCommand()

Таким образом, инициатор, отправляющий запрос, ничего не знает о получателе, который и будет выполнять команду. Кроме того, если нам потребуется применить какие-то новые команды, мы можем просто унаследовать классы от абстрактного класса Command и реализовать его методы Execute и Undo.

В программах на C# команды находят довольно широкое применение. Так, в технологии WPF и других технологиях, которые используют XAML и подход MVVM, на командах во многом базируется взаимодействие с пользователем. В некоторых архитектурах, например, в архитектуре CQRS, команды являются одним из ключевых компонентов.

Нередко в роли инициатора команд выступают панели управления или кнопки интерфейса. Самая простая ситуация — надо программно организовать включение и выключение прибора, например, телевизора. Решение данной задачи с помощью команд могло бы выглядеть так:

class Program
{
    static void Main(string[] args)
    {
        Pult pult = new Pult();
        TV tv = new TV();
        pult.SetCommand(new TVOnCommand(tv));
        pult.PressButton();
        pult.PressUndo();
         
        Console.Read();
    }
}
 
interface ICommand
{
    void Execute();
    void Undo();
}
 
// Receiver - Получатель
class TV
{ 
    public void On()
    {
        Console.WriteLine("Телевизор включен!");
    }
 
    public void Off()
    {
        Console.WriteLine("Телевизор выключен...");
    }
}
 
class TVOnCommand : ICommand
{
    TV tv;
    public TVOnCommand(TV tvSet)
    {
        tv = tvSet;
    }
    public void Execute()
    {
        tv.On();
    }
    public void Undo()
    {
        tv.Off();
    }
}
 
// Invoker - инициатор
class Pult
{
    ICommand command;
 
    public Pult() { }
 
    public void SetCommand(ICommand com)
    {
        command = com;
    }
 
    public void PressButton()
    {
        command.Execute();
    }
    public void PressUndo()
    {
        command.Undo();
    }
}

Итак, в этой программе есть интерфейс команды — ICommand, есть ее реализация в виде класса TVOnCommand, есть инициатор команды — класс Pult, некий прибор — пульт, управляющий телевизором. И есть получатель команды — класс TV, представляющий телевизор. В качестве клиента используется класс Program.

При этом пульт ничего не знает об объекте TV. Он только знает, как отправить команду. В итоге мы получаем гибкую систему, в которой мы легко можем заменять одни команды на другие, создавать последовательности команд. Например, в нашей программе кроме телевизора появилась микроволновка, которой тоже неплохо было бы управлять с помощью одного интерфейса. Для этого достаточно добавить соответствующие классы и установить команду:

class Program
{
    static void Main(string[] args)
    {
        Pult pult = new Pult();
        TV tv = new TV();
        pult.SetCommand(new TVOnCommand(tv));
        pult.PressButton();
        pult.PressUndo();
 
        Microwave microwave = new Microwave
        // 5000 - время нагрева пищи
        pult.SetCommand(new MicrowaveCommand(microwave, 5000));
        pult.PressButton();
         
        Console.Read();
    }
}
//.....ранее описанные классы
 
class Microwave
{
    public void StartCooking(int time)
    {
        Console.WriteLine("Подогреваем еду");
        // имитация работы с помощью асинхронного метода Task.Delay
        Task.Delay(time).GetAwaiter().GetResult();
    }
 
    public void StopCooking()
    {
        Console.WriteLine("Еда подогрета!");
    }
}
class MicrowaveCommand : ICommand
{
    Microwave microwave;
    int time;
    public MicrowaveCommand(Microwave m, int t)
    {
        microwave = m;
        time = t;
    }
    public void Execute()
    {
        microwave.StartCooking(time);
        microwave.StopCooking();
    }
 
    public void Undo()
    {
        microwave.StopCooking();
    }
}

Теперь еще одним получателем запроса является класс Microwave, функциональностью которого можно управлять через команды MicrowaveCommand.

Правда, в вышеописанной системе есть один изъян: если мы попытаемся выполнить команду до ее назначения, то программа выдаст исключение, так как команда будет не установлена. Эту проблему мы могли бы решить, проверяя команду на значение null в классе инициатора:

class Pult
{
    ICommand command;
 
    public Pult() { }
 
    public void SetCommand(ICommand com)
    {
        command = com;
    }
 
    public void PressButton()
    {
        if(command!=null)
            command.Execute();
    }
    public void PressUndo()
    {
        if(command!=null)
            command.Undo();
    }
}

Либо можно определить класс пустой команды, которая будет устанавливаться по умолчанию:

class NoCommand : ICommand
{
    public void Execute()
    {
    }
    public void Undo()
    {
    }
}
class Pult
{
    ICommand command;
 
    public Pult() 
    { 
        command = new NoCommand();
    }
 
    public void SetCommand(ICommand com)
    {
        command = com;
    }
 
    public void PressButton()
    {
        command.Execute();
    }
    public void PressUndo()
    {
        command.Undo();
    }
}

При этом инициатор необязательно указывает на одну команду. Он может управлять множеством команд. Например, на пульте от телевизора есть как кнопка для включения, так и кнопки для регулировки звука:

class Program
{
    static void Main(string[] args)
    {
        TV tv = new TV();
        Volume volume = new Volume();
        MultiPult mPult = new MultiPult();
        mPult.SetCommand(0, new TVOnCommand(tv));
        mPult.SetCommand(1, new VolumeCommand(volume));
        // включаем телевизор
        mPult.PressButton(0);
        // увеличиваем громкость
        mPult.PressButton(1);
        mPult.PressButton(1);
        mPult.PressButton(1);
        // действия отмены
        mPult.PressUndoButton();
        mPult.PressUndoButton();
        mPult.PressUndoButton();
        mPult.PressUndoButton();
 
        Console.Read();
    }
}
interface Command
{
    void Execute();
    void Undo();
}
 
class TV
{ 
    public void On()
    {
        Console.WriteLine("Телевизор включен!");
    }
 
    public void Off()
    {
        Console.WriteLine("Телевизор выключен...");
    }
}
 
class TVOnCommand : ICommand
{
    TV tv;
    public TVOnCommand(TV tvSet)
    {
        tv = tvSet;
    }
    public void Execute()
    {
        tv.On();
    }
    public void Undo()
    {
        tv.Off();
    }
}
class Volume
{
    public const int OFF = 0;
    public const int HIGH = 20;
    private int level;
 
    public Volume()
    {
        level = OFF;
    }
 
    public void RaiseLevel()
    {
        if (level < HIGH)
            level++;
        Console.WriteLine("Уровень звука {0}", level);
    }
    public void DropLevel()
    {
        if (level > OFF)
            level--;
        Console.WriteLine("Уровень звука {0}", level);
    }
}
 
class VolumeCommand : ICommand
{
    Volume volume;
    public VolumeCommand(Volume v)
    {
        volume = v;
    }
    public void Execute()
    {
        volume.RaiseLevel();
    }
 
    public void Undo()
    {
        volume.DropLevel();
    }
}
 
class NoCommand : ICommand
{
    public void Execute()
    {
    }
    public void Undo()
    {
    }
}
 
class MultiPult
{
    ICommand[] buttons;
    Stack<ICommand> commandsHistory;
 
    public MultiPult()
    {
        buttons = new ICommand[2];
        for (int i = 0; i < buttons.Length; i++)
        {
            buttons[i] = new NoCommand();
        }
        commandsHistory = new Stack<ICommand>();
    }
 
    public void SetCommand(int number, ICommand com)
    {
        buttons[number] = com;
    }
 
    public void PressButton(int number)
    {
        buttons[number].Execute();
        // добавляем выполненную команду в историю команд
        commandsHistory.Push(buttons[number]);
    }
    public void PressUndoButton()
    {
        if(commandsHistory.Count>0)
        {
            ICommand undoCommand = commandsHistory.Pop();
            undoCommand.Undo();
        }
    }
}

Здесь два получателя команд — классы TV и Volume. Volume управляет уровнем звука и сохраняет текущий уровень в переменной level. Также есть две команды TVOnCommand и VolumeCommand.

Инициатор — MultiPult имеет две кнопки в виде массива buttons: первая предназначена для TV, а вторая — для увеличения уровня звука. Чтобы сохранить историю команд используется стек. При отправке команды в стек добавляется новый элемент, а при ее отмене, наоборот, происходит удаление из стека. В данном случае стек выполняет роль примитивного лога команд.

Телевизор включен!
Уровень звука 1
Уровень звука 2
Уровень звука 3
Уровень звука 2
Уровень звука 1
Уровень звука 0
Телевизор выключен...

Макрокоманды

Для управления набором команд используются макрокоманды. Макрокоманда должна реализовать тот же интерфейс, что и другие команды, при этом макрокоманда инкапсулирует в одной из своих переменных весь набор используемых команд. Рассмотрим на примере.

Для создания и развития программного продукта необходимо несколько исполнителей, выполняющих различные функции: программист пишет код, тестировщик выполняет тестирование продукта, а маркетолог пишет рекламные материалы и проводит кампании по рекламированию продукта. Управляет всем процессом менеджер. Программа на C#, описывающая создание программного продукта с помощью паттерна команд, могла бы выглядеть следующим образом:

class Program
{
    static void Main(string[] args)
    {
        Programmer programmer = new Programmer();
        Tester tester = new Tester();
        Marketolog marketolog = new Marketolog();
 
        List<ICommand> commands = new List<ICommand> 
        {
            new CodeCommand(programmer),
            new TestCommand(tester),
            new AdvertizeCommand(marketolog)
        };
        Manager manager = new Manager();
        manager.SetCommand(new MacroCommand(commands));
        manager.StartProject();
        manager.StopProject();
         
        Console.Read();
    }
}
interface ICommand
{
    void Execute();
    void Undo();
}
// Класс макрокоманды
class MacroCommand : ICommand
{
    List<ICommand> commands;
    public MacroCommand(List<ICommand> coms)
    {
        commands = coms;
    }
    public void Execute()
    {
        foreach(ICommand c in commands)
            c.Execute();
    }
 
    public void Undo()
    {
        foreach (ICommand c in commands)
            c.Undo();
    }
}
 
class Programmer
{
    public void StartCoding()
    {
        Console.WriteLine("Программист начинает писать код");
    }
    public void StopCoding()
    {
        Console.WriteLine("Программист завершает писать код");
    }
}
 
class Tester
{
    public void StartTest()
    {
        Console.WriteLine("Тестировщик начинает тестирование");
    }
    public void StopTest()
    {
        Console.WriteLine("Тестировщик завершает тестирование");
    }
}
 
class Marketolog
{
    public void StartAdvertize()
    {
        Console.WriteLine("Маркетолог начинает рекламировать продукт");
    }
    public void StopAdvertize()
    {
        Console.WriteLine("Маркетолог прекращает рекламную кампанию");
    }
}
 
class CodeCommand : ICommand
{
    Programmer programmer;
    public CodeCommand(Programmer p)
    {
        programmer = p;
    }
    public void Execute()
    {
        programmer.StartCoding();
    }
    public void Undo()
    {
        programmer.StopCoding();
    }
}
 
class TestCommand : ICommand
{
    Tester tester;
    public TestCommand(Tester t)
    {
        tester = t;
    }
    public void Execute()
    {
        tester.StartTest();
    }
    public void Undo()
    {
        tester.StopTest();
    }
}
 
class AdvertizeCommand : ICommand
{
    Marketolog marketolog;
    public AdvertizeCommand(Marketolog m)
    {
        marketolog = m;
    }
    public void Execute()
    {
        marketolog.StartAdvertize();
    }
 
    public void Undo()
    {
        marketolog.StopAdvertize();
    }
}
 
class Manager
{
    ICommand command;
    public void SetCommand(ICommand com)
    {
        command = com;
    }
    public void StartProject()
    {
        if (command != null)
            command.Execute();
    }
    public void StopProject()
    {
        if (command != null)
            command.Undo();
    }
}

В роли инициатора здесь выступает менеджер, а в роли получателей запросов — программист, маркетолог и тестеровщик. Запуская проект, менеджер тем самым запускает макрокоманду, которая содержит ряд отдельных команд. Выполнение этих команд делегируется получателям.

Подробнее

Я пишу эту главу в приподнятом настроении, потому что Команда — это один из моих любимых шаблонов. В большинстве программ, которые я писал — и в играх и в других программах так или иначе находилось применение для этого шаблона. Не раз с его помощью мне удавалось распутать довольно корявый код.

Для такого важного шаблона банда четырех приготовила ожидаемо заумное описание:

Инкапсуляция запроса в внутри объекта, позволяющая пользователю параметризировать клиенты с помощью различных запросов, организовывать в очереди или регистрировать запросы или организовывать поддержку отменяемых операций.

Можно ли согласиться с таким ужасным приговором? Прежде всего он искажает все что данная метафора способна предложить. За пределами странного мира программ, где слова могут означать что угодно, слово «клиент» означает личность — кого-то с кем вы имеете дело. Причем обычно других людей не принято «параметризировать».

Дальше идет просто перечисление того, где можно применять шаблон. Не слишком очевидно, конечно если ваш вариант использования прямо не указан в этом перечне. Мое краткое определение данного шаблона гораздо проще:

Команда — это материализация вызова метода.

Конечно «краткое» не всегда означает «достаточное», так что толку от этого по прежнему мало. Давайте немного углубимся в суть дела. «Материализовать» чтобы вы знали означает буквально «сделать реальным».

Еще один термин материализации — это объявление чего либо «объектом первого класса». Оба термина означают что мы возьмем некую концепцию и превратим ее в набор данных — объект, который можно поместить в переменную, передать в функцию и т.д. Таким образом если мы говорим, что команда — это «материализация вызова метода» — это означает что вызов метода оборачивается некоторым объектом.

Есть много разных названий: «обратный вызов», «функция первого класса», «указатель на функцию», «замыкание (closure)», «частично примененная функция (partially applied function)», в зависимости от языка к которому вы привыкли. Однако все это одного поля ягоды. Банда четырех немного дальше уточняет:

Команда — это объектно- ориентированная замена обратного вызова.

Это уже гораздо полезнее для осмысленного выбора шаблона.

Но пока это все абстрактно и слишком туманно. Я хочу начать главу с чего-то конкретного, и сейчас я это сделаю. Чтобы это сделать мне понадобится пример, в котором применение команды будет смотреться идеально.

Настройка ввода

Внутри каждой игры есть код, отвечающий за считывание пользовательского ввода — нажатия на кнопки, клавиатурные события, нажатия мыши и т.д. Этот код обрабатывает ввод и преобразует его в соответствующие действия в игре:

Самая примитивная реализация выглядит следующим образом:

void InputHandler::handleInput() {
  if (isPressed(BUTTON_X)) jump();
  else if (isPressed(BUTTON_Y)) fireGun();
  else if (isPressed(BUTTON_A)) swapWeapon();
  else if (isPressed(BUTTON_B)) lurchIneffectively();
}

Такая функция обычно вызывается на каждом кадре внутри игрового цикла (Game Loop). Думаю вам понятно что она делает. Здесь мы видим жесткую привязку пользовательского ввода с действиями в игре. Однако многие игры позволяют пользователям настраивать какие кнопки за что отвечают.

Для того чтобы это стало возможным нам нужно преобразовать прямые вызовы jump() и fireGun() в нечто, что мы сможем свободно менять местами. «Менять местами» звучит как присвоение значений переменным, поэтому нам нужен объект, который будет представлять игровое действие. И тут в дело вступает шаблон Команда.

Для начала определим базовый класс, представляющий запускаемую игровую команду:

class Command {
  public:
    virtual~Command() {}
  virtual void execute() = 0;
};

Теперь создадим дочерние классы для каждой из различных игровых команд:

class JumpCommand: public Command {
  public: virtual void execute() {
    jump();
  }
};

class FireCommand: public Command {
  public: virtual void execute() {
    fireGun();
  }
};

В нашем обработчике ввода мы будем хранить указатели на команду для каждой кнопки:

class InputHandler {
  public:
    void handleInput();

  // Методы для привязки команд...

  private:
    Command * buttonX_;
  Command * buttonY_;
  Command * buttonA_;
  Command * buttonB_;
};

Теперь обработка ввода сводится к делегированию такого вида:

void InputHandler::handleInput() {
  if (isPressed(BUTTON_X)) buttonX_ - > execute();
  else if (isPressed(BUTTON_Y)) buttonY_ - > execute();
  else if (isPressed(BUTTON_A)) buttonA_ - > execute();
  else if (isPressed(BUTTON_B)) buttonB_ - > execute();
}

Там где раньше пользовательский ввод напрямую вызывал функции, теперь у нас появился промежуточный слой косвенности:

В этом и заключается сущность шаблона Команда. Если вы уже оценили его по достоинству, оставшуюся часть главы можете рассматривать как бонус.

Указания для актеров

Классы команд, которые мы только что определили, отлично работают для примера выше, но их возможности все-таки сильно ограничены. Проблема в том, что мы предполагаем что у нас уже есть готовые функции высокого уровня jump()fireGun() и т.д., которые сами знают как найти персонаж игрока и заставить его плясать под нашу дудку.

Такое предположение значительно снижает применимость наших команд. Получается что команда JumpCommand — это единственное, что способно заставить прыгать только нашего игрока. Давайте избавимся от этого ограничения. Вместо того чтобы запускать функцию, которая будет сама искать объект для воздействия, мы сами передадим ей объект, которым хотим управлять:

class Command {
  public:
    virtual~Command() {}
  virtual void execute(GameActor & actor) = 0;
};

Здесь в качестве GameActor выступает наш класс «игровой объект», представляющий игрока в игровом мире. Мы передаем его в execute() и таким образом изолированная команда получает возможность вызвать метод выбранного нами актера:

class JumpCommand: public Command {
  public: virtual void execute(GameActor & actor) {
    actor.jump();
  }
};

Теперь мы можем использовать этот единственный класс чтобы заставить прыгать любого в нашей игре. Правда у нас пока еще нет прослойки между обработчиком ввода и командой, которая собственно получает команду и применяет ее к нужному объекту. Для начала мы изменим handleInput() таким образом чтобы она возвращала команду:

Command * InputHandler::handleInput() {
  if (isPressed(BUTTON_X)) return buttonX_;
  if (isPressed(BUTTON_Y)) return buttonY_;
  if (isPressed(BUTTON_A)) return buttonA_;
  if (isPressed(BUTTON_B)) return buttonB_;

  // Если ничего не передано, то ничего и не делаем.
  return NULL;
}

Функция не может выполнить команду немедленно потому что не знает какого актера ей передать. Зато мы можем воспользоваться тем преимуществом команды, что это материализованный вызов — мы можем отложить выполнение.

Теперь нам нужен код, который получит команду и передаст в нее актера, представляющего игрока. Нечто наподобие:

Command * command = inputHandler.handleInput();
if (command) {
  command - > execute(actor);
}

Предполагая что actor указывает на персонажа игрока, мы получаем корректную реализацию того чего добивались, т.е. мы вернулись к тому же поведению что и в самом первом примере. Добавив слой косвенности между командой и актером, который ее выполняет, мы получили еще одну приятную способность: теперь мы можем позволить игроку управлять любым актером в игре, просто подменяя актера, к которому применяется команда.

На практике такая возможность используется не слишком часто. Но похожий вариант использования все равно часто всплывает. До сих пор мы упоминали только управляемых игроком персонажей. А что насчет остальных? Тех, которые управляются игровым AI. Мы можем использовать тот же самый шаблон в качестве интерфейса между движком AI и актерами: код AI просто будет вызывать объекты Command.

Уменьшение связности в данном случае, когда AI выбирает команду, а код актера ее выполняет, дает нам дополнительную гибкость. Мы получаем возможность использовать разные модули AI для разных актеров. Или же мы можем их смешивать и выстраивать AI для разных стилей поведения. Вам нужен более агрессивный противник? Просто подключите более агрессивный AI чтобы им управлять. На самом деле мы можем даже передать на попечение AI персонаж игрока, что довольно удобно для демо-режима, когда игра работает на автопилоте.

Делая команду, управляющую актером объектом первого класса, мы избавляемся от жесткой привязки прямого вызова методов. Вместо этого можете думать об этом как об очереди или потоке команд.

Некоторый код (обработчик ввода или AI) генерирует команды и добавляет их в поток. Другой код (диспетчер или сам актер) поглощает команды и вызывает их. Поместив такую очередь в середину, мы уменьшили связность между производителем с одной стороны и потребителем с другой.

Отмена и повтор

Последний пример — это самый известный способ применения данного шаблона. Если объект команда может выполнять действия, значит мы уже сделали маленький шаг к тому чтобы получить возможность их отменять. Отмену можно встретить в некоторых стратегических играх, когда вы имеете возможность отменить последнее не понравившееся вам действие. Такая функциональность обязательно присутствует и в инструментах, которые используются для создания игр. Лучший способ заставить гейм-дизайнера ненавидеть вас — это выдать ему инструментарий, в котором нельзя отменить того что он наворотил своими толстенькими пальчиками.

Без шаблона команда, реализация отмены довольно сложна. С ним — пара пустяков. Для примера предположим что мы разрабатываем однопользовательскую пошаговую игру и хотим разрешить игроку отменять ходы чтобы он мог больше сосредоточиться на стратегии, а не на угадывании.

Мы уже оценили удобство использования команды для абстрагирования пользовательского ввода, поэтому каждый ход игрока у нас уже инкапсулирован в команду. Например движение юнита может выглядеть следующим образом:

class MoveUnitCommand: public Command {
  public: MoveUnitCommand(Unit * unit, int x, int y): unit_(unit),
  x_(x),
  y_(y) {}

  virtual void execute() {
    unit_ - > moveTo(x_, y_);
  }

  private: Unit * unit_;
  int x_,
  y_;
};

Обратите внимание на небольшое отличие от нашей предыдущей команды. В предыдущем примере мы хотели абстрагировать команду от актера, на которого она действует. В этом же случае мы специально хотим привязать ее к актеру, которого она двигает. Экземпляр этой команды — это не обобщенная операция «перемещающая что либо», которую можно применить в самом разном контексте, а конкретный отдельный шаг в очереди шагов игры.

Это показывает насколько вариативным может быть применение данного шаблона. В некоторых случаях как наша первая парочка примеров, команда — это переиспользуемый объект, представляющий действие которое можно выполнить. Наш первый пример обработки ввода сводился к единственному вызову метода execute() по нажатию нужной кнопки.

А вот более специфическая команда. Она описывает вещи, которые можно сделать в определенный момент. Это значит что код обработчика ввода будет создаваться каждый раз, когда игрок решит двинутся. Выглядеть это будет следующим образом:

Command * handleInput() {
  // Выбираем юнит...
  Unit * unit = getSelectedUnit();

  if (isPressed(BUTTON_UP)) {
    // Перемещаем юнит на единицу вверх.
    int destY = unit - > y() - 1;
    return new MoveUnitCommand(unit, unit - > x(), destY);
  }

  if (isPressed(BUTTON_DOWN)) {
    // Перемещаем юнит на единицу вниз.
    int destY = unit - > y() + 1;
    return new MoveUnitCommand(unit, unit - > x(), destY);
  }

  // Другие шаги...

  return NULL;
}

Тот факт что команды получаются одноразовыми дает нам определенные преимущества. Чтобы сделать команды отменяемыми, мы определим еще одну операцию, которую должен реализовывать каждый класс команд:

class Command {
  public:
    virtual~Command() {}
  virtual void execute() = 0;
  virtual void undo() = 0;
};

Метод undo() возвращает игру в то состояние, в котором она была до выполнения соответствующего метода execute() Вот наша последняя команда, дополненная поддержкой отмены:

class MoveUnitCommand: public Command {
  public: MoveUnitCommand(Unit * unit, int x, int y): unit_(unit),
  xBefore_(0),
  yBefore_(0),
  x_(x),
  y_(y) {}

  virtual void execute() {
    // Запоминаем позицию юнита перед ходом
    // чтобы потом ее восстановить.
    xBefore_ = unit_ - > x();
    yBefore_ = unit_ - > y();

    unit_ - > moveTo(x_, y_);
  }

  virtual void undo() {
    unit_ - > moveTo(xBefore_, yBefore_);
  }

  private: Unit * unit_;
  int xBefore_,
  yBefore_;
  int x_,
  y_;
};

Обратите внимание что мы добавили в класс больше состояний. После того как мы переместили юнит, ему неоткуда узнать где он был раньше. Чтобы иметь возможность отменить перемещение, нам нужно запомнить предыдущую позицию самостоятельно. Вот для этого мы и добавляем в команду xBefore_ и yBefore_.

Чтобы позволить игроку отменить движение, нам нужно сохранить последнюю выполненную им команду. И потом когда мы жмакнем Ctrl-Z, мы просто вызовем метод undo(). (Если мы уже выполнили отмену, то по нажатию на ту же кнопку можно выполнить команду повтор и выполнить команду снова.)

Поддержка множественной отмены не намного сложнее. Вместо того чтобы просто запоминать последнюю команду, мы будем хранить список команд и ссылку на «текущую». Когда игрок выполняет команду, она добавляется в список команд и помечается как «текущая».

Когда игрок выбирает «Отмена», мы отменяем текущую команду и сдвигаем указатель на одну позицию назад. Когда мы выполняем повтор, мы перемещаем указатель на позицию вперед и выполняем команду. Когда игрок после отмены выполняет новую команду, все содержимое списка после текущей команды выбрасывается.

Когда я первый раз реализовал это в редакторе уровней, я почувствовал себя волшебником. Я был изумлен насколько прямолинейным является это решение и насколько хорошо оно работает. Конечно вам потребуется некоторая дисциплина чтобы оформить все модификации в виде команд, но как только вы с этим справитесь — дальше все будет просто.

Круто или бесполезно?

Как я говорил раньше, команды похожи на функции первого класса или замыкания, однако во всех примерах мы использовали определение классов. Если вы знакомы с функциональным программированием, вам наверное интересно где же функции.

Я написал примеры таким образом потому что поддержка функций первого класса в C++ весьма ограничена. Указатели на функции не имеют состояния, функторы- странные и все равно требуют определения классов, а лямбды в C++11 сложны в работе из-за ограничений ручного управления памятью.

При этом я не утверждаю что вы не можете использовать для реализации шаблона команда функции в других языках. Если вам доступна роскошь в виде языка с поддержкой настоящих замыканий — используйте их конечно! В некоторых случаях шаблон команда вообще используется в языках, не поддерживающих замыкания для их эмуляции.

Например, если мы пишем игру на JavaScripts, мы можем написать команду движения следующим образом:

function makeMoveUnitCommand(unit, x, y) {
  // эта функция представляет собой объект команды:
  return function () {
    unit.moveTo(x, y);
  }
}

С помощью пары замыканий мы можем реализовать отмену и повтор:

function makeMoveUnitCommand(unit, x, y) {
  var xBefore, yBefore;
  return {
    undo: function () {
      xBefore = unit.x();
      yBefore = unit.y();
      unit.moveTo(x, y);
    },
    redo: function () {
      unit.moveTo(xBefore, yBefore);
    }
  };
}

Если вам комфортно работать в функциональном стиле, такой способ покажется для вас естественным. Если нет, я надеюсь эта глава вам немного помогла. Для меня осознание полезности шаблона команда стало важным шагом в понимании полезности всей парадигмы функционального программирования в целом.

Смотрите также

  • Вы можете наплодить достаточно большое количество классов команд. Чтобы упростить их определение, можно создать общий базовый класс с кучей удобный высокоуровневых методов, которые наследующие его классы могут комбинировать для формирования своего поведения. В таком случае главный метод команды execute превращается в подкласс Песочница (Subclass Sandbox).
  • В наших примерах, мы явно указывали какой актер должен выполнять команду. В некоторых случаях, особенно когда модель объекта организована иерархически, все может быть не столь очевидно. Объект может ответить на команду, а может перепоручить ее выполнение какому либо другому подчиненному объекту. Если вы это сделаете, вы получите Цепочку ответственности (Chain of Responsibility.) GOF.
  • Некоторые команды представляют собой прямолинейное поведение как в примере с JumpCommand. В этом случае иметь больше одного экземпляра класса — пустая трата памяти, потому что все экземпляры идентичны. В такой ситуации вам пригодится класс Приспособленец(Flyweight).

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *