← Назад к вопросам

Приведи пример паттерна Command в реальной жизни

1.7 Middle🔥 191 комментариев
#Паттерны проектирования

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Отличный вопрос! Паттерн Command (Команда) — один из поведенческих паттернов, который инкапсулирует запрос как объект, позволяя параметризовать клиентов с различными запросами, ставить запросы в очередь или логировать их, а также поддерживать отмену операций. Давайте рассмотрим его на классическом и очень наглядном примере из игрового движка Unity — системе управления вводом и системой Undo/Redo в редакторе уровней.

🎮 Реальный пример: Система перемещения объектов в редакторе уровней с поддержкой Undo/Redo

Представьте, что вы создаете инструмент для дизайнера уровней. Дизайнер может перемещать, вращать и масштабировать объекты на сцене. Критически важно, чтобы все действия можно было отменить (Ctrl+Z) или повторить (Ctrl+Y). Реализация этого через прямой вызов методов трансформации приведет к хаосу. Здесь на помощь приходит паттерн Command.

💡 Основная идея

Мы инкапсулируем каждое действие (например, "переместить объект из точки A в точку B") в отдельный объект-команду. Этот объект знает:

  • Как выполнить действие (Execute).
  • Как отменить его (Undo).
  • С каким объектом он работает.

🧱 Структура классов

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

// 1. Абстрактный базовый класс Command
public abstract class Command
{
    public abstract void Execute();
    public abstract void Undo();
}

// 2. Конкретная команда для перемещения GameObject
public class MoveCommand : Command
{
    private GameObject _objectToMove;
    private Vector3 _previousPosition;
    private Vector3 _newPosition;

    // Конструктор сохраняет всё необходимое состояние
    public MoveCommand(GameObject objectToMove, Vector3 previousPosition, Vector3 newPosition)
    {
        _objectToMove = objectToMove;
        _previousPosition = previousPosition;
        _newPosition = newPosition;
    }

    // Выполнить действие
    public override void Execute()
    {
        // В реальности здесь может быть анимация или проверки
        _objectToMove.transform.position = _newPosition;
        Debug.Log($"Выполнено: Перемещение {_objectToMove.name} в {_newPosition}");
    }

    // Отменить действие
    public override void Undo()
    {
        _objectToMove.transform.position = _previousPosition;
        Debug.Log($"Отменено: Возврат {_objectToMove.name} в {_previousPosition}");
    }
}

🎯 Класс-инвокер (Invoker) - Управляющий командами

Теперь нам нужен объект, который будет хранить историю команд и управлять их выполнением и отменой. В Unity это может быть синглтон CommandManager.

// 3. Инвокер (Invoker) - управляет историей и выполнением
public class CommandManager : MonoBehaviour
{
    private static CommandManager _instance;
    public static CommandManager Instance => _instance;

    // Стеки для истории команд (Undo и Redo)
    private Stack<Command> _undoStack = new Stack<Command>();
    private Stack<Command> _newCommandsSinceUndo = new Stack<Command>();

    private void Awake()
    {
        if (_instance != null && _instance != this)
            Destroy(this.gameObject);
        else
            _instance = this;
    }

    // Выполнить и записать новую команду
    public void ExecuteCommand(Command command)
    {
        command.Execute();
        _undoStack.Push(command); // Добавляем в историю для Undo
        _newCommandsSinceUndo.Clear(); // Очищаем "будущее" при новом действии
    }

    // Отмена (Undo)
    public void Undo()
    {
        if (_undoStack.Count > 0)
        {
            Command lastCommand = _undoStack.Pop();
            lastCommand.Undo();
            _newCommandsSinceUndo.Push(lastCommand); // Сохраняем для возможного Redo
        }
    }

    // Повтор (Redo)
    public void Redo()
    {
        if (_newCommandsSinceUndo.Count > 0)
        {
            Command nextCommand = _newCommandsSinceUndo.Pop();
            nextCommand.Execute();
            _undoStack.Push(nextCommand); // Возвращаем команду в основную историю
        }
    }
}

🕹️ Клиентский код (например, скрипт управления UI или вводом)

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

// Пример в скрипте, обрабатывающем перетаскивание объекта
public class ObjectDragger : MonoBehaviour
{
    private Vector3 _dragStartPosition;
    private GameObject _selectedObject;

    void OnMouseDown()
    {
        _selectedObject = gameObject;
        _dragStartPosition = transform.position;
    }

    void OnMouseDrag()
    {
        // ... логика перетаскивания (например, рейкаст на плоскость)
    }

    void OnMouseUp()
    {
        Vector3 dragEndPosition = transform.position; // Новая позиция после перетаскивания

        // ВМЕСТО ПРЯМОГО ИЗМЕНЕНИЯ:
        // transform.position = dragEndPosition;

        // СОЗДАЕМ И ВЫПОЛНЯЕМ КОМАНДУ:
        MoveCommand moveCommand = new MoveCommand(_selectedObject, _dragStartPosition, dragEndPosition);
        CommandManager.Instance.ExecuteCommand(moveCommand);
    }
}

✅ Преимущества такого подхода в реальном проекте

  • Полная реализация Undo/Redo: История команд в стеках позволяет легко откатывать и повторять сложные последовательности действий.
  • Декомпозиция и чистота кода: Логика действий изолирована в отдельных классах. Код управления вводом (ObjectDragger) ничего не знает о том, как отменить действие.
  • Гибкость и расширяемость: Добавить новое действие (например, RotateCommand или ChangeColorCommand) — значит просто создать новый класс, унаследованный от Command. CommandManager будет работать с ним без изменений.
  • Возможность отложенного выполнения и очереди: Команды можно ставить в очередь (например, для воспроизведения записанной последовательности действий) или выполнять с задержкой.
  • Логирование: Поскольку каждое действие — объект, его легко сериализовать и записать в лог для отладки или анализа действий пользователя.

Этот паттерн выходит далеко за рамки редакторов. Он применяется в:

  • Управлении игровыми способностями (каждая способность — команда, которую можно поставить в очередь или отменить).
  • Системе replay (воспроизведение ввода игрока как последовательности команд).
  • Управлении AI, где AI планирует последовательность команд-действий для юнитов.

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