Приведи пример паттерна Command в реальной жизни
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Отличный вопрос! Паттерн 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.