撤消重做实现的最佳实践

我需要为我的窗口应用程序(像powerpoint这样的编辑器)实现Undo / Redo框架工作,应该遵循的最佳实践是什么,如何处理我的对象的所有属性更改以及它在UI上的reflection。

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

此外,另外需要注意的是,因为它不需要您使用相反的命令来撤消先前完成的操作,这意味着任何可能的单向函数[例如散列或加密]都无法通过倒数进行简单的撤消通过回滚到较旧的快照,可以非常简单地撤消命令。

同样如指出的那样, 命令模式可能资源密集程度较低,因此我将在特定情况下承认:

  • 存在大的对象状态和/或
  • 没有破坏性的方法和
  • 可以非常简单地使用相互命令来反转所采取的任何行动

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

我可能会避免使用这两者的混搭,因为我倾向于关心开发人员,这些开发人员会支持我,并维护我的代码以及我的雇主的道德责任,使这个过程变得简单和便宜可能。 我看到这两种模式的混搭很容易变成一个难以维护的老鼠洞,让人感觉很难维持。

经典的做法是遵循命令模式 。

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

这里有三种可行的方法。 纪念模式(快照),命令模式和状态差异。 它们都有优点和缺点。

如果你可以放弃它,我会选择State Diffing,因为它结合了内存减少, 易于实现和可维护性

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

以下摘自文章:

纪念品模式

在此处输入图像描述

优点

  • 实施与应用的行动无关。 实施后,我们可以添加操作而不必担心破坏历史记录。
  • 快速前进到历史中的预定义位置。 当在当前和期望的历史位置之间应用的动作在计算上是昂贵的时,这是有趣的。

缺点

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

命令模式

在此处输入图像描述

优点

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

缺点

  • 我们不能直接进入任意位置,而是需要取消应用历史堆栈直到我们到达那里。 这可能很耗时。
  • 每个动作和它的反向都需要封装在一个对象中。 如果你的行动不重要,这可能很困难。 (反向)动作中的错误实际上很难调试,很容易导致致命的崩溃。 即使是简单的行动通常也会涉及很多复杂性。 例如,在3D编辑器的情况下,添加到模型的对象需要存储添加的内容,当前选择的颜色,覆盖的内容,镜像模式是否有效等。
  • 当动作没有简单的反向时,例如模糊图像时,实施和记忆密集可能具有挑战性。

状态差异

在此处输入图像描述

优点

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

缺点/限制

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

参考:

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

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

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

  • 添加/删除形状
  • 形状的属性变化

基类:

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 UndoStack = new Stack(); Stack RedoStack = new Stack(); 

添加新形状(例如Point)

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

我认为这是实现undo-redo算法的最有效方法。 例如,请查看我的网站上的这个页面: DrawIt 。

我在Tekening.cs文件的第479行附近实现了undo redo stuff。 您可以下载源代码。 它可以通过任何类型的应用程序实现。