Подтвердить что ты не робот

Передовая практика реализации Undo Redo

Мне нужно реализовать работу фрейма Undo/Redo для моего оконного приложения (редактора, такого как powerpoint), что должно быть наилучшей практикой, как бы справиться со всеми изменениями свойств моих объектов и отражением в пользовательском интерфейсе.

4b9b3361

Ответ 1

Существует два классических шаблона. Первый - это memento pattern, который используется для хранения моментальных снимков вашего полного состояния объекта. Это, пожалуй, более интенсивно работает в системе, чем шаблон команды, но позволяет очень просто откатить старый снимок. Вы можете хранить снимки на диске a la PaintShop/PhotoShop или хранить их в памяти для небольших объектов, для которых не требуется постоянство. То, что вы делаете, - это именно то, для чего был разработан этот шаблон, поэтому он должен соответствовать законопроекту немного лучше, чем шаблон команды, предложенный другими.

Кроме того, следует отметить, что, поскольку он не требует, чтобы у вас были взаимные команды, чтобы отменить то, что было сделано ранее, это означает, что любой потенциально один из способов [хэширование или шифрование], которые нельзя отменить тривиально, используя взаимные команды, все равно можно отменить очень просто, просто вернувшись к более раннему снимку.

Также, как указано, шаблон команды, который потенциально менее ресурсоемкий, поэтому я уступлю этому в конкретных случаях, когда:

  • Существует большое состояние объекта и/или
  • Нет деструктивных методов и
  • Если взаимные команды могут использоваться очень тривиально, чтобы отменить все предпринятые действия

шаблон команды может быть лучше подходит [но не обязательно, это будет сильно зависеть от ситуации]. В других случаях я бы использовал шаблон памяти.

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

Ответ 2

Существует три подхода, которые являются жизнеспособными. Шаблон сувениров (Снимки), Шаблон команд и Различия состояний. Все они имеют свои преимущества и недостатки, и в действительности все зависит от вашего варианта использования, с какими данными вы работаете и что вы хотите использовать.

Я бы пошел с State Diffing, если вы можете обойтись без него, поскольку он сочетает в себе уменьшение памяти с простотой реализации и ремонтопригодностью.

Я собираюсь процитировать статью, описывающую три подхода (ссылка ниже).

Обратите внимание, что VoxelShop, упомянутый в статье, является открытым исходным кодом. Итак, вы можете взглянуть на сложность шаблона команды здесь: https://github.com/simlu/voxelshop/tree/develop/src/main/java/com/vitco/app/core/data/history

Ниже приведен адаптированный отрывок из статьи. Однако я рекомендую вам прочитать его полностью.


Шаблон памятного подарка

enter image description here

Каждое состояние истории хранит полную копию. Действие создает новое состояние, а указатель используется для перемещения между состояниями, что позволяет отменить и повторить.

Доводы

  • Реализация не зависит от примененного действия. После реализации мы можем добавлять действия, не беспокоясь о нарушении истории.
  • Это быстро перейти к предопределенной позиции в истории. Это интересно, когда действия, выполняемые между текущей и желаемой историческими позициями, требуют больших вычислительных ресурсов.

Против

  • Требования к памяти могут быть значительно выше по сравнению с другими подходами.
  • Время загрузки может быть медленным, если снимки большие.

Шаблон команды

enter image description here

Аналогично образцу Memento, но вместо сохранения полного состояния сохраняется только разница между состояниями. Разница сохраняется как действия, которые можно применять и не применять. При введении нового действия необходимо применить и не применять.

Доводы

  • Объем памяти мал. Нам нужно только сохранить изменения в модели, и если они небольшие, то стек истории также невелик.

Против

  • Мы не можем перейти к произвольной позиции напрямую, а должны отменить применение стека истории, пока не доберемся до нее. Это может занять много времени.
  • Каждое действие и его обратное должно быть заключено в объекте. Если ваше действие не тривиально, это может быть сложно. Ошибки в (обратном) действии действительно трудно отладить и могут легко привести к фатальным сбоям. Даже простые действия обычно сопряжены с большим количеством сложностей. Например. в случае 3D-редактора объект для добавления в модель должен хранить то, что было добавлено, какой цвет был выбран в настоящий момент, что было перезаписано, если активирован зеркальный режим и т.д.
  • Может быть сложным для реализации и потреблять много памяти, когда у действий нет простого обратного действия, например, при размытии изображения.

State Diffing

enter image description here

Аналогичен шаблону команд, но разница сохраняется независимо от действия путем простого изменения состояний. Введение нового действия не требует каких-либо особых соображений.

Доводы

  • Реализация не зависит от примененного действия. После добавления функции истории мы можем добавлять действия, не беспокоясь о нарушении истории.
  • Требования к памяти, как правило, намного ниже, чем для подхода "Снимок", и во многих случаях сопоставимы с подходом "Командный шаблон". Однако это сильно зависит от типа применяемых действий. Например. инвертирование цвета изображения с использованием командного шаблона должно быть очень дешевым, тогда как State Diffing сохранит все изображение. И наоборот, при рисовании длинной линии свободной формы подход с использованием командного шаблона может использовать больше памяти, если он объединяет записи истории для каждого пикселя.

Минусы/Ограничения

  • Мы не можем перейти к произвольной позиции напрямую, а должны отменить применение стека истории, пока не доберемся до этого.
  • Нам нужно вычислить разницу между состояниями. Это может быть дорого.
  • Внедрение xor diff между состояниями модели может быть сложным в зависимости от вашей модели данных.

Ссылка:

https://www.linkedin.com/pulse/solving-history-hard-problem-lukas-siemon

Ответ 3

Классическая практика - следовать шаблону команд.

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

Ответ 4

Взгляните на Command Pattern. Вы должны инкапсулировать каждое изменение вашей модели в отдельные объекты команд.

Ответ 5

Я написал действительно гибкую систему для отслеживания изменений. У меня есть программа для рисования, которая реализует 2 типа изменений:

  • добавить/удалить форму
  • изменение свойства фигуры

Базовый класс:

public abstract class Actie
{
    public Actie(Vorm[] Vormen)
    {
        vormen = Vormen;
    }

    private Vorm[] vormen = new Vorm[] { };
    public Vorm[] Vormen
    {
        get { return vormen; }
    }

    public abstract void Undo();
    public abstract void Redo();
}

Производный класс для добавления фигур:

public class VormenToegevoegdActie : Actie
{
    public VormenToegevoegdActie(Vorm[] Vormen, Tekening tek)
        : base(Vormen)
    {
        this.tek = tek;
    }

    private Tekening tek;
    public override void Redo()
    {
        tek.Vormen.CanRaiseEvents = false;
        tek.Vormen.AddRange(Vormen);
        tek.Vormen.CanRaiseEvents = true;
    }

    public override void Undo()
    {
        tek.Vormen.CanRaiseEvents = false;
        foreach(Vorm v in Vormen)
            tek.Vormen.Remove(v);
        tek.Vormen.CanRaiseEvents = true;
    }
}

Производный класс для удаления фигур:

public class VormenVerwijderdActie : Actie
{
    public VormenVerwijderdActie(Vorm[] Vormen, Tekening tek)
        : base(Vormen)
    {
        this.tek = tek;
    }

    private Tekening tek;
    public override void Redo()
    {
        tek.Vormen.CanRaiseEvents = false;
        foreach(Vorm v in Vormen)
            tek.Vormen.Remove(v);
        tek.Vormen.CanRaiseEvents = true;
    }

    public override void Undo()
    {
        tek.Vormen.CanRaiseEvents = false;
        foreach(Vorm v in Vormen)
            tek.Vormen.Add(v);
        tek.Vormen.CanRaiseEvents = true;
    }
}

Производный класс для изменений свойства:

public class PropertyChangedActie : Actie
{
    public PropertyChangedActie(Vorm[] Vormen, string PropertyName, object OldValue, object NewValue)
        : base(Vormen)
    {
        propertyName = PropertyName;
        oldValue = OldValue;
        newValue = NewValue;
    }

    private object oldValue;
    public object OldValue
    {
        get { return oldValue; }
    }

    private object newValue;
    public object NewValue
    {
        get { return newValue; }
    }

    private string propertyName;
    public string PropertyName
    {
        get { return propertyName; }
    }

    public override void Undo()
    {
        //Type t = base.Vorm.GetType();
        PropertyInfo info = Vormen.First().GetType().GetProperty(propertyName);
        foreach(Vorm v in Vormen)
        {
            v.CanRaiseVeranderdEvent = false;
            info.SetValue(v, oldValue, null);
            v.CanRaiseVeranderdEvent = true;
        }
    }
    public override void Redo()
    {
        //Type t = base.Vorm.GetType();
        PropertyInfo info = Vormen.First().GetType().GetProperty(propertyName);
        foreach(Vorm v in Vormen)
        {
            v.CanRaiseVeranderdEvent = false;
            info.SetValue(v, newValue, null);
            v.CanRaiseVeranderdEvent = true;
        }
    }
}

С каждым разом Vormen= массив элементов, представляемых для изменения. И это следует использовать так:

Объявление стеков:

Stack<Actie> UndoStack = new Stack<Actie>();
Stack<Actie> RedoStack = new Stack<Actie>();

Добавление новой фигуры (например, точки)

VormenToegevoegdActie vta = new VormenToegevoegdActie(new Vorm[] { NieuweVorm }, this);
UndoStack.Push(vta);
RedoStack.Clear();

Удаление выбранной фигуры

VormenVerwijderdActie vva = new VormenVerwijderdActie(to_remove, this);
UndoStack.Push(vva);
RedoStack.Clear();

Регистрация изменения свойства

PropertyChangedActie ppa = new PropertyChangedActie(new Vorm[] { (Vorm)e.Object }, e.PropName, e.OldValue, e.NewValue);
UndoStack.Push(ppa);
RedoStack.Clear();

Наконец, отменить/повторить действие

public void Undo()
{
    Actie a = UndoStack.Pop();
    RedoStack.Push(a);
    a.Undo();
}

public void Redo()
{
    Actie a = RedoStack.Pop();
    UndoStack.Push(a);
    a.Redo();
}

Я думаю, что это самый эффективный способ реализации алгоритма отмены повторения. Например, посмотрите на эту страницу на моем веб-сайте: DrawIt.

Я реализовал функцию отмены повторения в строке 479 файла Tekening.cs. Вы можете скачать исходный код. Это может быть реализовано любым приложением.