Перейти к содержанию

Command (Команда)

Категория: паттерн поведения.

Проблема

Действие пользователя (нажатие кнопки, пункт меню, горячая клавиша) нужно не просто выполнить, а: - поставить в очередь и выполнить позже; - сохранить в истории и дать возможность отменить (undo) или повторить (redo); - передать как параметр, не привязываясь к тому, что конкретно вызывающий код должен знать о деталях действия.

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

Решение

  1. Каждое действие оформляется отдельным объектом-командой с общим интерфейсом (обычно - метод Execute(), часто вместе с Undo()).
  2. Команда хранит внутри всё необходимое для выполнения действия: ссылку на получателя (объект, который реально что-то делает) и параметры операции.
  3. Инициатор (например, кнопка UI или обработчик очереди задач) работает только с абстракцией ICommand и вызывает Execute(), не зная деталей конкретного действия.
  4. Благодаря тому, что команда - полноценный объект, её можно сохранить в списке (история для undo/redo), поставить в очередь, сериализовать или передать по сети.

Структура

  • Command - интерфейс с методом Execute() (и опционально Undo()).
  • ConcreteCommand - хранит получателя и параметры, реализует Execute/Undo через вызовы методов получателя.
  • Receiver - объект, который реально выполняет действие; ничего не знает о существовании команд.
  • Invoker - хранит и запускает команды (и, возможно, историю для отмены), не зная их внутреннего устройства.

Когда применять

  • Нужна отмена/повтор действий (undo/redo) - для этого само действие обязано быть объектом, а не просто вызовом метода.
  • Действия нужно ставить в очередь, откладывать во времени или выполнять асинхронно.
  • Нужно логировать все выполненные операции или строить на их основе транзакционность (например, отменить всю пачку команд при ошибке).
  • Один и тот же UI-элемент (кнопка, пункт меню) должен запускать разные действия, подставляемые снаружи.

Плюсы

  • Отделяет объект, инициирующий действие, от объекта, который его выполняет.
  • Позволяет собирать историю команд для undo/redo, логирования или отложенного выполнения.
  • Новое действие добавляется новым классом-командой, без изменения инициатора.

Минусы

  • Для простого, разового действия без нужды в отмене или очереди - создание отдельного класса-команды избыточно; в C# такие случаи обычно проще выразить делегатом (Action).
  • Полноценная реализация Undo (особенно для составных операций) может потребовать хранить существенный объём состояния "до" изменения, что увеличивает сложность команд.

Пример в .NET Framework / BCL

  • ICommand в WPF/MAUI (RelayCommand, DelegateCommand) - связывает элементы UI (кнопки, пункты меню) с обработчиками, включая поддержку CanExecute.
  • Паттерн лежит в основе очередей задач и джобов (например, в фоновых обработчиках, где "команда" сериализуется, кладётся в очередь и выполняется позже, возможно, в другом процессе).
  • Транзакции с поддержкой отмены в редакторах (текстовых, графических, IDE) - именно так реализован Undo/Redo в большинстве таких приложений.

Пример реализации на C#

Command.cs
using System;
using System.Collections.Generic;

namespace DesignPatterns.Behavioral.Command
{
    // Получатель - объект, который реально выполняет действия.
    // Он ничего не знает о существовании команд или истории отмен.
    public sealed class TextDocument
    {
        public string Content { get; private set; } = string.Empty;

        public void InsertText(string text, int position) =>
            Content = Content.Insert(position, text);

        public void RemoveText(int position, int length) =>
            Content = Content.Remove(position, length);
    }

    // Общий интерфейс команды - каждое действие умеет выполниться и отмениться
    public interface ICommand
    {
        void Execute();
        void Undo();
    }

    // Конкретная команда хранит всё, что нужно для выполнения и отмены действия:
    // ссылку на получателя и параметры операции.
    public sealed class InsertTextCommand : ICommand
    {
        private readonly TextDocument _document;
        private readonly string _text;
        private readonly int _position;

        public InsertTextCommand(TextDocument document, string text, int position)
        {
            _document = document;
            _text = text;
            _position = position;
        }

        public void Execute() => _document.InsertText(_text, _position);

        public void Undo() => _document.RemoveText(_position, _text.Length);
    }

    // Инициатор - хранит историю выполненных команд и умеет их отменять.
    // Он работает только с абстракцией ICommand, не зная о конкретных действиях.
    public sealed class CommandHistory
    {
        private readonly Stack<ICommand> _history = new();

        public void Execute(ICommand command)
        {
            command.Execute();
            _history.Push(command);
        }

        public void UndoLast()
        {
            if (_history.Count == 0)
            {
                Console.WriteLine("Нечего отменять");
                return;
            }

            ICommand last = _history.Pop();
            last.Undo();
        }
    }

    public static class Demo
    {
        public static void Run()
        {
            var document = new TextDocument();
            var history = new CommandHistory();

            history.Execute(new InsertTextCommand(document, "Привет, ", position: 0));
            history.Execute(new InsertTextCommand(document, "мир!", position: 8));
            Console.WriteLine(document.Content); // "Привет, мир!"

            history.UndoLast();
            Console.WriteLine(document.Content); // "Привет, "

            history.UndoLast();
            Console.WriteLine($"«{document.Content}»"); // ""
        }
    }
}

Открыть Command.cs отдельно Скачать Command.cs