Приведи пример реализации отмены действия (паттерн Command).

Ответ

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

Реализация на C# с поддержкой отмены:

// 1. Абстрактная команда
public interface ICommand
{
    void Execute();
    void Undo();
}

// 2. Получатель команды — объект, над которым выполняется операция.
public class TextEditor
{
    public string Text { get; set; } = string.Empty;
}

// 3. Конкретная команда для изменения текста
public class ChangeTextCommand : ICommand
{
    private readonly TextEditor _editor;
    private string _previousText;
    private readonly string _newText;

    public ChangeTextCommand(TextEditor editor, string newText)
    {
        _editor = editor;
        _newText = newText;
    }

    public void Execute()
    {
        // Перед выполнением сохраняем состояние для возможной отмены
        _previousText = _editor.Text;
        _editor.Text = _newText;
        Console.WriteLine($"Execute: Text changed to '{_newText}'.");
    }

    public void Undo()
    {
        // Восстанавливаем предыдущее состояние
        _editor.Text = _previousText;
        Console.WriteLine($"Undo: Text reverted to '{_previousText}'.");
    }
}

// 4. Инициатор команд (Invoker) — управляет выполнением и историей.
public class CommandManager
{
    private readonly Stack<ICommand> _undoStack = new Stack<ICommand>();

    public void ExecuteCommand(ICommand command)
    {
        command.Execute();
        _undoStack.Push(command); // Добавляем команду в историю для отмены
    }

    public void Undo()
    {
        if (_undoStack.Count > 0)
        {
            ICommand lastCommand = _undoStack.Pop();
            lastCommand.Undo();
        }
        else
        {
            Console.WriteLine("Nothing to undo.");
        }
    }
}

// Пример использования
class Program
{
    static void Main()
    {
        var editor = new TextEditor();
        var manager = new CommandManager();

        // Выполняем команды
        manager.ExecuteCommand(new ChangeTextCommand(editor, "Hello"));
        manager.ExecuteCommand(new ChangeTextCommand(editor, "Hello, World!"));
        manager.ExecuteCommand(new ChangeTextCommand(editor, "Hello, Universe!"));

        Console.WriteLine($"nCurrent text: {editor.Text}"); // Hello, Universe!

        // Отменяем действия
        manager.Undo(); // Возврат к "Hello, World!"
        Console.WriteLine($"After undo: {editor.Text}");

        manager.Undo(); // Возврат к "Hello"
        Console.WriteLine($"After second undo: {editor.Text}");
    }
}

Ключевые преимущества подхода:

  • Разделение ответственности: Код, инициирующий действие (CommandManager), не зависит от кода, который его выполняет (ChangeTextCommand) и от получателя (TextEditor).
  • Реализация сложных операций: Команда может объединять несколько действий над разными объектами, а её метод Undo() корректно откатывает их все.
  • Расширяемость: Легко добавить новые типы команд, логирование, повторное выполнение (Redo) или отложенный запуск команд.

Ответ 18+ 🔞

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

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

// 1. Это типа наш контракт для всех команд. Любая команда должна уметь две вещи: сделать и отменить.
public interface ICommand
{
    void Execute();
    void Undo();
}

// 2. А это — получатель, тот, над кем мы будем издеваться. В нашем случае — текстовый редактор.
public class TextEditor
{
    public string Text { get; set; } = string.Empty;
}

// 3. А вот и конкретная пакостная команда, которая меняет текст.
public class ChangeTextCommand : ICommand
{
    private readonly TextEditor _editor;
    private string _previousText; // Вот это — ключевое! Сохраняем старое состояние, чтобы было что откатывать.
    private readonly string _newText;

    public ChangeTextCommand(TextEditor editor, string newText)
    {
        _editor = editor;
        _newText = newText;
    }

    public void Execute()
    {
        // Перед тем как нагадить, запоминаем, как было чисто.
        _previousText = _editor.Text;
        _editor.Text = _newText;
        Console.WriteLine($"Execute: Текст поменяли на '{_newText}'.");
    }

    public void Undo()
    {
        // А если нагадили, то убираем за собой. Возвращаем как было.
        _editor.Text = _previousText;
        Console.WriteLine($"Undo: Откатили текст обратно на '{_previousText}'.");
    }
}

// 4. А это — наш главный распорядитель, командир. Он решает, когда какую команду запускать и хранит историю, чтобы было что отменять.
public class CommandManager
{
    private readonly Stack<ICommand> _undoStack = new Stack<ICommand>(); // История команд — наш стек отмены.

    public void ExecuteCommand(ICommand command)
    {
        command.Execute();
        _undoStack.Push(command); // Запихнули команду в историю — теперь её можно отменить.
    }

    public void Undo()
    {
        if (_undoStack.Count > 0)
        {
            ICommand lastCommand = _undoStack.Pop(); // Достаём последнюю команду из стека.
            lastCommand.Undo(); // И говорим ей: "Отмена, сука!"
        }
        else
        {
            Console.WriteLine("Отменять нечего, иди отсюда.");
        }
    }
}

// Ну и смотрим, как это всё работает вживую.
class Program
{
    static void Main()
    {
        var editor = new TextEditor();
        var manager = new CommandManager();

        // Начинаем творить историю.
        manager.ExecuteCommand(new ChangeTextCommand(editor, "Привет"));
        manager.ExecuteCommand(new ChangeTextCommand(editor, "Привет, Мир!"));
        manager.ExecuteCommand(new ChangeTextCommand(editor, "Привет, Вселенная!"));

        Console.WriteLine($"nТекущий текст: {editor.Text}"); // Привет, Вселенная!

        // Ой, передумал! Откатываем.
        manager.Undo(); // Возвращаемся к "Привет, Мир!"
        Console.WriteLine($"После отмены: {editor.Text}");

        manager.Undo(); // Возвращаемся к "Привет"
        Console.WriteLine($"После второй отмены: {editor.Text}");
    }
}

Вот в чём, блядь, сила этого подхода:

  • Всё по полочкам: Тот, кто команду даёт (CommandManager), вообще не в курсе, что она там делает внутри. Он просто тыкает кнопки Execute и Undo. А реальная работа идёт в команде и редакторе. Разделение ответственности — красота!
  • Сложные операции — не проблема: Одна команда может внутри себя делать кучу действий над разными объектами. А метод Undo() аккуратно всё это откатит, как будто так и было. Никаких костылей.
  • Расширяется проще пареной репы: Захотел добавить логирование — допиши в CommandManager. Нужна очередь команд на выполнение — пожалуйста. Нужен повтор (Redo) — заведи второй стек. Архитектура к этому готова, потому что команды — это независимые кирпичики, а не монолитная хуйня.