Паттерн «Команда» (Command). Описание и примеры использования

Команда (Command) – один из «классических» поведенческих паттернов, описанных ещё у «Банды Четырёх» [1]. Он используется для создания гибкого механизма действий над чем-либо или команд. В этом механизме класс отправитель команды и класс получатель не зависят друг от друга.

В данном паттерне объект «Команды» инкапсулирует как само действие, так и его параметры.

Описание и сфера применения

Паттерн «Команда» состоит из следующих компонентов:

  • Command (Команда)
    Базовый класс. Объявляет интерфейс для выполняемой операции (команды);
  • ConcretteCommand (Конкретная команда)
    Определяет связь между объектом получателем (Receiver) и действием. Реализует интерфейс, объявленный Command;
  • Client (Клиент)
    Создаёт объект ConcretteCommand и задаёт для него получателя;
  • Invoker (Инициатор)
    Обращается к команде для выполнения операции;
  • Receiver (Получатель)
    Располагает информацией о способах выполнения команды. В его роли может выступить любой класс.

Паттерн «Команда» может пригодиться в следующих областях [2].

  • GUI (в основном, кнопки пользовательского интерфейса и пункты меню);
  • Запись макросов;
  • Многоуровневая отмена операций (Undo);
  • Сети;
  • Индикаторы выполнения;
  • Пулы потоков;
  • Транзакции;
  • Мастера (мастера настроек, установки программ и т.п.).
Пример «классической» реализации на C#

Рассмотрим следующую программу, которая будет в самых общих чертах моделировать «управление» самолётом.

Получателем команд будет самолёт, и он будет получать две команды: «Взлёт» (TakeOff) и «Посадка» (Landing).

Состояние самолёта (полёт или пребывание на земле) будет задаваться обычной текстовой строкой. Чтобы не усложнять восприятие примера и без того не самого простого паттерна, паттерн «Состояние» применять не станем.

Ниже приведён код класса самолёта (Receiver).

class Airplane
{
    public string State { get; set; }
}

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

abstract class Command
{
    protected Airplane _plane;
    public abstract void Excecute();
    public abstract void Undo();
    public Command(Airplane plane)
    {
        _plane = plane;
    }
}

Команда «Взлёт»:

class TakeOff : Command
{
    public TakeOff(Airplane plane) : base(plane)
    {
    }
    public override void Excecute()
    {
        _plane.State="Flying";
    }
    public override void Undo()
    {
        _plane.State="Landed";
    }
}

Команда «Посадка»:

class Landing : Command
{
    public Landing(Airplane plane) : base(plane)
    {
    }
    public override void Excecute()
    {
        _plane.State = "Landed";
    }
    public override void Undo()
    {
        _plane.State = "Flying";
    }
}

В обеих командах реализована одноуровневая отмена действия.

Класс инициализатор команд (Invoker):

class Invoker
{
    private Command _command;
    public Command Command
    {
        set
        {
            _command = value;
        }
    }
    public void Run()
    {
        _command.Excecute();
    }
    public void Undo()
    {
        _command.Undo();
    }
}

Методы Run и Undo запускают на выполнение и отменяют выполненную команду соответственно. Сама команда задаётся через свойство Command, которое связано с закрытым полем _command.

Далее приведена консольная программа, которая наглядно иллюстрирует работу паттерна.

Управление программой производится цифровыми символами. Программа обрабатывает код нажатой клавиши. При вводе цифры «1» самолёт «взлетает». При вводе цифры «2» самолёт «садится». При вводе цифры «3» происходит отмена выполненной команды. Цифра «4» — завершение работы программы.

class Program
{
    static void Main(string[] args)
    {
        Airplane plane = new Airplane();
        Invoker flyingControl = new Invoker();
        int a = 0;
        do
        {
            a = PlaneControl(plane, flyingControl);
            Console.ReadKey();
        }
        while (a!=52);
    }
    private static int PlaneControl(Airplane plane, Invoker receiver)
    {
        Console.WriteLine("Что делать самолёту? ");
        int a = Console.Read();
        switch (a)
        {
           case 49:
               {
                    receiver.Command = new TakeOff(plane);
                   receiver.Run();
                }
                ; break;
            case 50:
                {
                    receiver.Command = new Landing(plane);
                    receiver.Run();
                }
                ; break;
            case 51:
                receiver.Undo();
                ; break;
        }
        Console.WriteLine(plane.State);
        return a;
    }
}

Корректное выполнение команд в Invoker

Пример, который был приведён выше вполне рабочий, но в нём есть один существенный недочёт. Что произойдёт при вызове метода Run или Undo, если команда по тем или иным причинам не была своевременно задана или инициализирована?

Правильный ответ – исключение. Ведь в этом случае мы будем работать с несуществующим объектом.

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

Если не получается корректно разрешить эту проблему локально с помощью защитного программирования, создайте собственный класс исключения, которое будет выдавать Invoker в случае, когда объект команды не инициализирован (вы и тем более другой человек, который будет работать с вашим кодом должны понимать из-за чего именно в программе произошёл сбой).

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

public void Run()
{
    if (_command != null)
    {
        _command.Excecute();
    }
    else
    {
        _command = new DefaultCommand();
        _command.Excecute();
    }
}

А, для случая с исключением примерно так:

public void Run()
{
    if (_command != null)
    {
        _command.Excecute();
    }
    else
    {
        throw new CommandIsNotInitializedException();
    }
}

Использование вырожденного паттерна «Команда» в GUI (на примере C++ Qt)

Рассмотрим пример использования паттерна «Команда» для графического интерфейса (GUI). Для этого напишем простую программу с интерфейсом из двух кнопок.

Каждая из кнопок создаёт объекты команд и запускает их на выполнение.

Программу будем писать на языке программирования C++ с использованием библиотеки Qt 5.8.

Первая команда будет отображать стандартное окно с информацией о библиотеке Qt, а вторая также стандартный диалог с текстовым сообщением (своего рода аналог MessageBox.Show() в C#). Оба окна должны располагаться по центру относительно родительского окна.

Сами команды и Invoker не представляют ничего особенного.

Базовый класс команд:

// Заголовок (.h)
class Command
{
    protected:
        QWidget *parent;
    public:
        Command();
        virtual ~Command();
        virtual void Execute()=0;
};
// Реализация (.cpp)
Command::Command()
{
    this->parent=parentW;
}
Command::~Command()
{
    delete parent;
}

Первая команда:

// Заголовок (.h)
class ShowAboutQtCommand : public Command
{
    public:
        void Execute() override;
        ShowAboutQtCommand(QWidget *parentW);
};
// Реализация (.cpp)
ShowAboutQtCommand::ShowAboutQtCommand(QWidget *parentW):Command(parentW)
{
}
void ShowAboutQtCommand::Execute()
{
    QMessageBox::aboutQt(parent);
}

Вторая команда:

// Заголовок (.h)
class ShowTextMessageCommand : public Command
{
    public:
        void Execute() override;
        ShowTextMessageCommand(QWidget *parentW);
};
// Реализация (.cpp)
ShowTextMessageCommand::ShowTextMessageCommand(QWidget *parentW):Command(parentW)
{
}
void ShowTextMessageCommand::Execute()
{
    QMessageBox::information(parent,"Text message","Text",QMessageBox::Ok);
}

Invoker:

// Заголовок (.h)
class Invoker
{
private:
    Command *command;
public:
    Invoker();
    ~Invoker();
    void setCommand(Command *com);
    void run();
};
// Реализация (.cpp)
Invoker::Invoker()
{
}
Invoker::~Invoker()
{
    delete command;
}
void Invoker::setCommand(Command *com)
{
    command=com;
}
void Invoker::run()
{
    if(command!=0)
    {
        command->Execute();
    }
}

Все вышеприведённые компоненты паттерна не отличаются от «классической» версии. Но, совсем другое дело Receiver и Client.

В предыдущем примере роль Receiver играл объект класса Airplane, а в качестве клиента выступало приложение. Здесь же Receiver как самостоятельный элемент будет отсутствовать, а его функции вместе с функциями клиента возьмёт на себя родительское окно.

Дело в том, что в Qt для того, чтобы расположить окно с сообщением по центру родительского окна, необходимо передать родительское окно в качестве параметра соответствующего статического метода класса QMessageBox (как это показано в коде классов команд).

Так как система отображения диалоговых окон при помощи класса QMessageBox достаточно унифицирована, а родительские окна могут быть произвольными, то при использовании паттерна «Команда» очень удобно передавать родительское в качестве Receiver’а, хотя «настоящим» Receiver’ом является скорее QMessageBox.

Ниже приведён код главного (и единственного) окна рассматриваемой программы.

// Заголовок (.h)
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
public slots:
    void CreateAboutQtCommand();
    void CreateTextInfoCommand();
private:
    Ui::MainWindow *ui;
    Invoker *commandManager;
};
// Реализация (.cpp)
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    commandManager=new Invoker();
}
MainWindow::~MainWindow()
{
    delete ui;
    delete commandManager;
}
void MainWindow::CreateAboutQtCommand()
{
    commandManager->setCommand(new ShowAboutQtCommand(this));
    commandManager->run();
}
void MainWindow::CreateTextInfoCommand()
{
    commandManager->setCommand(new ShowTextMessageCommand(this));
    commandManager->run();
}

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

Нередко для того чтобы получить эффективное решение недостаточно иметь готовый «сборник рецептов» и нужно пофантазировать над задачей.

Однако к нестандартным решениям следует всё же подходить обдуманно и ни в коем случае не злоупотреблять. Иначе велик шанс, например, изобрести очередной «велосипед с квадратными колёсами».

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

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