C# 中撤消重做实现的最佳实践

发布于 2024-08-15 12:33:18 字数 1436 浏览 2 评论 0原文

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

旧情勿念 2024-08-22 12:33:18

有两种经典模式可供使用。第一个是 备忘录模式,用于存储完整对象状态的快照。这可能比命令模式更加系统密集,但它允许非常简单地回滚到较旧的快照。您可以像 PaintShop/PhotoShop 一样将快照存储在磁盘上,或者将它们保留在内存中以存储不需要持久性的较小对象。您所做的正是此模式的设计目的,因此它应该比其他人建议的命令模式稍微更符合要求。

另外,需要注意的是,因为它不需要您使用相互命令来撤消之前完成的操作,所以这意味着任何潜在的单向函数(例如散列或加密)都无法使用相互命令轻松撤消只需回滚到较旧的快照,仍然可以非常简单地撤消命令。

另外,正如所指出的, 命令模式 可能资源密集程度较低,所以我承认在特定情况下:

  • 存在需要持久化的大型对象状态和/或
  • 没有破坏性方法并且
  • 可以非常简单地使用交互命令来反转所采取的任何操作,

命令模式可能是更适合[但不一定,这很大程度上取决于情况]。在其他情况下,我会使用纪念品模式。

我可能会避免使用两者的混搭,因为我倾向于关心将在我身后维护我的代码的开发人员,以及我对雇主的道德责任,使该过程变得简单且廉价可能的。我发现这两种模式的混搭很容易成为一个难以维护的老鼠洞,维护起来会很昂贵。

There are two classic patterns to use. The first is the memento pattern which is used to store snapshots of your complete object state. This is perhaps more system intensive than the command pattern, but it allows rollback very simply to an older snapshot. You could store the snapshots on disk a la PaintShop/PhotoShop or keep them in memory for smaller objects that don't require persistence. What you're doing is exactly what this pattern was designed for, so it should fit the bill slightly better than the Command Pattern suggested by others.

Also, an additional note is that because it doesn't require you to have reciprocal commands to undo something that was previously done, it means that any potentially one way functions [such as hashing or encryption] which can't be undone trivially using reciprocal commands can still be undone very simply by just rolling back to an older snapshot.

Also as pointed out, the command pattern which is potentially less resource intensive, so I will concede that in specific cases where:

  • There is a large object state to be persisted and/or
  • There are no destructive methods and
  • Where reciprocal commands can be used very trivially to reverse any action taken

the command pattern may be a better fit [but not necessarily, it will depend very much on the situation]. In other cases, I would use the memento pattern.

I would probably refrain from using a mashup of the two because I tend to care about the developer that's going to come in behind me and maintain my code as well as it being my ethical responsibility to my employer to make that process as simple and inexpensive as possible. I see a mashup of the two patterns easily becoming an unmaintainable rat hole of discomfort that would be expensive to maintain.

与之呼应 2024-08-22 12:33:18

这里有三种方法是可行的。备忘录模式(快照)、命令模式和状态差异。它们都有优点和缺点,这实际上取决于您的用例、您正在使用的数据以及您愿意实施的内容。

如果你能摆脱它,我会选择状态差异,因为它结合了内存减少和易于实现和可维护性

我将引用一篇描述这三种方法的文章(参考下文)。

请注意,文章中提到的 VoxelShop 是开源的。所以你可以在这里看看命令模式的复杂性:
https://github .com/simlu/voxelshop/tree/develop/src/main/java/com/vitco/app/core/data/history

下面是该文章的改编摘录。不过,我建议您完整阅读它。


备忘录模式

在此处输入图像描述

每个历史状态都存储一个完整副本。操作创建一个新状态,并使用指针在状态之间移动以允许撤消和重做。

优点

  • 实现独立于所应用的操作。一旦实施,我们就可以添加操作,而不必担心破坏历史记录。
  • 晋升到历史上预定的位置是很快的。当当前和期望的历史位置之间应用的操作在计算上是昂贵的时,这很有趣。

缺点

  • 与其他方法相比,内存要求可能明显更高。
  • 如果快照很大,加载时间可能会很慢。

命令模式

在此处输入图像描述

与 Memento 模式类似,但不存储完整状态,而是仅存储状态之间的差异。差异被存储为可以应用和不应用的操作。引入新动作时,需要实现apply和un-apply。

优点

  • 内存占用小。我们只需要存储对模型的更改,如果这些更改很小,那么历史堆栈也很小。

缺点

  • 我们不能直接转到任意位置,而是需要取消应用历史堆栈,直到到达那里。这可能非常耗时。
  • 每个动作及其相反的动作都需要封装在一个对象中。如果您的行动不平凡,那么这可能会很困难。 (反向)操作中的错误确实很难调试,并且很容易导致致命的崩溃。即使看似简单的动作通常也涉及相当多的复杂性。例如,在 3D 编辑器的情况下,用于添加到模型的对象需要存储添加的内容、当前选择的颜色、覆盖的内容、镜像模式是否处于活动状态等。
  • 当操作不执行时,实施起来可能会很困难,并且会占用大量内存有一个简单的反转,例如当模糊图像时。

状态差异

在此处输入图像描述

与命令模式类似,但差异是通过简单地对状态进行异或操作而独立于操作存储的。引入新操作不需要任何特殊考虑。

优点

  • 实施独立于所应用的操作。添加历史记录功能后,我们可以添加操作,而不必担心破坏历史记录。
  • 内存要求通常比快照方法低得多,并且在很多情况下与命令模式方法相当。然而,这在很大程度上取决于所应用的操作类型。例如,使用命令模式反转图像的颜色应该非常便宜,而状态差异将保存整个图像。相反,当绘制长的自由形状线时,如果命令模式方法为每个像素链接历史条目,则它可能会使用更多内存。

缺点/限制

  • 我们不能直接转到任意位置,而是需要取消应用历史堆栈,直到到达那里。
  • 我们需要计算状态之间的差异。这可能很昂贵。
  • 根据您的数据模型,在模型状态之间实现异或差异可能很难实现。

参考:

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

There are three approaches here that are viable. Memento Pattern (Snapshots), Command Pattern and State Diffing. They all have advantages and disadvantages and it really comes down to your use case, what data you are working with and what you are willing to implement.

I would go with State Diffing if you can get away with it as it combines memory reduction with ease of implementation and maintainability.

I'm going to quote an article describing the three approaches (Reference below).

Note that VoxelShop mentioned in the article is open source. So you can take a look at the complexity of the command pattern here:
https://github.com/simlu/voxelshop/tree/develop/src/main/java/com/vitco/app/core/data/history

Below is an adapted excerpt from the article. However I do recommend that you read it in full.


Memento Pattern

enter image description here

Each history state stores a full copy. An action creates a new state and a pointer is used to move between the states to allow for undo and redo.

Pros

  • Implementation is independent of the applied action. Once implemented we can add actions without worrying about breaking history.
  • It is fast to advance to a predefined position in history. This is interesting when the actions applied between current and desired history position are computationally expensive.

Cons

  • Memory Requirements can be significantly higher compared to other approaches.
  • Loading time can be slow if the snapshots are large.

Command Pattern

enter image description here

Similar to the Memento Pattern, but instead of storing the full state, only the difference between the states is stored. The difference is stored as actions that can be applied and un-applied. When introducing a new action, apply and un-apply need to be implemented.

Pros

  • Memory footprint is small. We only need to store the changes to the model and if these are small, then the history stack is also small.

Cons

  • We can not go to an arbitrary position directly, but rather need to un-apply the history stack until we get there. This can be time consuming.
  • Every action and it's reverse needs to be encapsulated in an object. If your action is non trivial this can be difficult. Mistakes in the (reverse) action are really hard to debug and can easily result in fatal crashes. Even simple looking actions usually involve a good amount of complexity. E.g. in case of the 3D Editor, the object for adding to the model needs to store what was added, what color was currently selected, what was overwritten, if mirror mode active etc.
  • Can be challenging to implement and memory intensive when actions do not have a simple reverse, e.g when blurring an image.

State Diffing

enter image description here

Similar to the Command Pattern, but the difference is stored independent of the action by simply xor-nig the states. Introducing a new action does not require any special considerations.

Pros

  • Implementation is independent of the applied action. Once the history functionality is added we can add actions without worrying about breaking history.
  • Memory Requirements is usually much lower than for the Snapshot approach and in a lot of cases comparable to the Command Pattern approach. However this highly depends on the type of actions applied. E.g. inverting the color of an image using the Command Pattern should be very cheap, while State Diffing would save the whole image. Conversely when drawing a long free-form line, the Command Pattern approach might use more memory if it chained history entries for each pixel.

Cons / Limitations

  • We can not go to an arbitrary position directly, but rather need to un-apply the history stack until we get there.
  • We need to compute the diff between states. This can be expensive.
  • Implementing the xor diff between model states might be hard to implement depending on your data model.

Reference:

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

一袭白衣梦中忆 2024-08-22 12:33:18

经典做法是遵循命令模式

您可以封装任何使用命令执行操作的对象,并让它使用 Undo() 方法执行相反的操作。您可以将所有操作存储在堆栈中,以便轻松地倒回它们。

The classic practice is to follow the Command Pattern.

You can encapsulate any object that performs an action with a command, and have it perform the reverse action with an Undo() method. You store all the actions in a stack for an easy way of rewinding through them.

似最初 2024-08-22 12:33:18

查看命令模式
您必须将对模型的每个更改封装到单独的命令对象中。

Take a look at the Command Pattern.
You have to encapsulate every change to your model into separate command objects.

别念他 2024-08-22 12:33:18

我编写了一个非常灵活的系统来跟踪更改。我有一个绘图程序,它实现了两种类型的更改:

  • 添加/删除形状属性
  • 更改形状

基类:

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

我在文件 Tekening.cs 的第 479 行左右实现了撤消重做功能。您可以下载源代码。它可以通过任何类型的应用程序来实现。

I wrote a really flexible system to keep track of changes. I have a drawing program which implements 2 types of changes:

  • add/remove a shape
  • property change of a shape

Base class:

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();
}

Derived class for adding shapes:

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;
    }
}

Derived class for removing shapes:

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;
    }
}

Derived class for property changes:

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;
        }
    }
}

With each time Vormen = the array of items that are submitted to the change.
And it should be used like this:

Declaration of the stacks:

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

Adding a new shape (eg. Point)

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

Removing a selected shape

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

Registering a property change

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

Finally the Undo/Redo action

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

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

I think this is the most effective way of implementing a undo-redo algorithm.
For an example, look at this page on my website: DrawIt.

I implemented the undo redo stuff at around line 479 of the file Tekening.cs. You can download the source code. It can be implemented by any kind of application.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文