撤消重做实现的最佳实践
我需要为我的窗口应用程序(像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。 您可以下载源代码。 它可以通过任何类型的应用程序实现。