撤消引擎的设计模式

撤消引擎的设计模式

Design Pattern for Undo Engine

我正在为民用工程应用程序编写结构建模工具。 我有一个代表整个建筑物的巨大模型类,其中包括节点,线元素,荷载等的集合,它们也是自定义类。

我已经编码了一个撤消引擎,该引擎在对模型进行每次修改后都保存了一个深拷贝。 现在,我开始考虑是否可以编写不同的代码。 除了保存深层副本之外,我还可以保存每个修改器动作的列表以及相应的反向修改器。 这样我可以将反向修改器应用于当前模型以撤消,或将修改器应用于重做。

我可以想象您将如何执行更改对象属性等的简单命令。但是复杂命令呢? 就像将新的节点对象插入模型并添加一些线对象以保留对新节点的引用一样。

如何实施呢?


我见过的大多数示例为此使用了Command-Pattern的变体。每个无法撤消的用户操作都会获得其自己的命令实例,其中包含所有信息以执行该操作并将其回滚。然后,您可以维护所有已执行命令的列表,然后可以将它们逐一回滚。


我认为在处理OP所暗示的规模和范围模型时,纪念品和命令都不实用。它们会起作用,但是维护和扩展将需要大量工作。

对于此类问题,我认为您需要构建对数据模型的支持,以支持模型中涉及的每个对象的差异检查点。我曾经做过一次,而且效果很好。您要做的最大事情是避免在模型中直接使用指针或引用。

每个对另一个对象的引用都使用一些标识符(例如整数)。每当需要该对象时,都可以从表中查找该对象的当前定义。该表包含每个对象的链接列表,每个对象都包含所有以前的版本,以及有关它们针对哪个检查点进行活动的信息。

撤消/重做的实现很简单:执行您的操作并建立一个新的检查点;将所有对象版本回滚到先前的检查点。

它在代码中需要一定的纪律,但是却具有许多优点:因为您正在对模型状态进行差异存储,所以您不需要深层副本。您可以根据重做次数或使用的内存来确定要使用的内存量(对于CAD模型之类非常重要)。模型上运行的功能具有高度的可伸缩性和低维护性,因为它们无需执行任何操作即可实现撤消/重做。


如果您在谈论GoF,则Memento模式专门用于解决撤消问题。


正如其他人所述,命令模式是实现撤消/重做的非常强大的方法。但是我想提及命令模式有一个重要的优势。

使用命令模式实现撤消/重做时,可以通过抽象化(在某种程度上)对数据执行的操作并在撤消/重做系统中利用这些操作来避免大量重复的代码。例如,在文本编辑器中,剪切和粘贴是互补的命令(除了剪贴板的管理之外)。换句话说,剪切的撤消操作是粘贴,剪切的撤消操作被剪切。这适用于更简单的操作,例如键入和删除文本。

这里的关键是您可以将撤消/重做系统用作编辑器的主要命令系统。无需编写"创建撤消对象,修改文档"之类的系统,而是可以"创建撤消对象,对撤消对象执行重做操作以修改文档"。

现在,诚然,许多人都在想:"哦,这不是命令模式的重点吗?"是的,但是我看到太多的命令系统具有两组命令,一组用于立即操作,另一组用于撤消/重做。我并不是说不会有特定于立即操作和撤消/重做的命令,但是减少重复将使代码更易于维护。


您可能要参考Paint.NET代码进行撤消-他们有一个非常不错的撤消系统。它可能比您所需的要简单一些,但它可能会给您一些想法和指导。

-亚当


这可能是CSLA适用的情况。它旨在为Windows Forms应用程序中的对象提供复杂的撤消支持。


我已经成功地使用Memento模式实现了复杂的撤消系统-非常简单,并且具有自然提供Redo框架的好处。一个更微妙的好处是,聚合操作也可以包含在单个"撤消"中。

简而言之,您会有两叠纪念对象。一个用于撤消,另一个用于重做。每个操作都会创建一个新的备忘录,理想情况下是调用一些更改模型,文档(或其他任何东西)的状态。这将添加到撤消堆栈中。当您执行撤消操作时,除了对Memento对象执行撤消操作以再次更改模型外,您还从撤消堆栈中弹出对象并将其直接推入重做堆栈。

如何实现更改文档状态的方法完全取决于您的实现。如果您可以简单地进行API调用(例如ChangeColour(r,g,b)),则在其前面进行查询以获取并保存相应的状态。但是该模式还支持制作深层副本,内存快照,临时文件创建等-这完全取决于您,因为它只是虚拟方法的实现。

要执行汇总操作(例如,用户Shift-选择要执行操作的对象的负载,例如删除,重命名,更改属性),您的代码将创建一个新的撤消堆栈作为单个备忘录,并将其传递给实际操作将各个操作添加到。因此,您的操作方法无需(a)担心有全局堆栈,并且(b)可以以相同的方式进行编码,无论它们是独立执行还是作为一个聚合操作的一部分执行。

许多撤消系统仅在内存中,但是我想,如果您愿意,可以将撤消堆栈持久化。


刚读过我的敏捷开发书中的命令模式-也许这很有潜力?

您可以使每个命令都实现命令接口(具有Execute()方法)。如果要撤消,可以添加撤消方法。

更多信息在这里


我支持Mendelt Siebenga,您应该使用命令模式。您使用的模式是Memento模式,随着时间的流逝,它会而且非常浪费。

由于您正在处理内存密集型应用程序,因此您应该能够指定允许撤消引擎占用多少内存,可以存储多少级撤消级别或将它们保留到哪些存储中。如果不这样做,您将很快面临由于机器内存不足而导致的错误。

我建议您检查一下是否有一个框架已经使用您选择的编程语言/框架为撤消模型创建了模型。发明新东西是很好的,但是最好在真实场景中采用已经编写,调试和测试过的东西。如果添加了您正在编写的内容,这将有所帮助,以便人们可以推荐他们知道的框架。


Codeplex项目:

这是一个简单的框架,可以根据经典的Command设计模式向您的应用程序添加撤消/重做功能。它支持合并操作,嵌套事务,延迟执行(在顶级事务提交上执行)以及可能的非线性撤消历史记录(您可以在其中选择多个要重做的操作)。


作为参考,这是C#中撤消/重做的命令模式的简单实现:C#的简单撤消/重做系统。


处理撤消(使您的软件也适合多用户协作)的一种聪明方法是对数据结构进行操作转换。

这个概念不是很流行,但定义明确且有用。如果定义对您来说太抽象了,那么该项目就是一个成功示例,说明如何使用Javascript定义和实现JSON对象的操作转换


我读过的大多数示例都是通过使用命令或记忆模式来实现的。但是,您也可以使用简单的双端队列结构在没有设计模式的情况下进行操作。


您可以尝试在PostSharp中完成撤消/重做模式的现成实现。 https://www.postsharp.net/model/undo-redo

它允许您在应用程序中添加撤消/重做功能,而无需自己实现模式。它使用Recordable模式来跟踪模型中的更改,并且与INotifyPropertyChanged模式一起使用,该模式也在PostSharp中实现。

提供了UI控件,您可以决定每个操作的名称和粒度。


为钉跳益智游戏编写求解器时,我必须这样做。我使每个命令都成为一个Command对象,该对象包含足够的信息,可以完成或撤消它。就我而言,这就像存储起始位置和每次移动的方向一样简单。然后,我将所有这些对象存储在堆栈中,以便程序在回溯时可以轻松撤消所需的任意移动。


我们重新使用了文件加载并保存了"对象"的序列化代码,以方便的形式保存和恢复对象的整个状态。我们将这些序列化的对象连同有关执行了什么操作的一些信息一起推送到撤消堆栈上,如果没有足够的信息从序列化数据中收集信息,则会提示撤消该操作。撤消和重做通常只是将一个对象替换为另一个对象(理论上)。

由于指向对象的指针(C ++)在执行一些奇怪的撤消重做序列(那些位置未更新为更安全的撤消感知"标识符")时从未固定的许多错误。这个区域的虫子经常...嗯...有趣。

对于速度/资源的使用,某些操作可能是特殊情况-例如,调整大小,四处移动。

多选也提供一些有趣的复杂性。幸运的是,我们在代码中已经有了分组概念。克里斯托弗·约翰逊(Kristopher Johnson)关于子项目的评论与我们所做的非常接近。


设计模式的第一部分(GoF,1994)有一个用例,用于将撤消/重做实现为设计模式。


我发现命令模式在这里非常有用。我没有执行几个反向命令,而是在第二个API实例上使用了具有延迟执行的回滚。

如果您希望实现工作量少且易于维护(并且可以为第二个实例提供额外的内存),则此方法似乎是合理的。

请参见此处的示例:
https://github.com/thilo20/撤消/


我曾经在一个应用程序上工作过,在该应用程序中,通过更新模型中维护的内部数据库中的字段,命令对应用程序模型(即,我们正在使用MFC的CDocument ...)所做的所有更改都保留在命令末尾。因此,我们不必为每个操作编写单独的撤消/重做代码。每次更改记录时(在每个命令的末尾),撤消堆栈仅记住主键,字段名称和旧值。


您可以使您的最初想法表现出色。

使用持久数据结构,并坚持保留对旧状态的引用列表。 (但是,只有在操作状态类中的所有数据都是不可变的并且对它的所有操作都返回一个新版本的情况下,这才真正起作用。但是新版本不需要是深层副本,只需替换已更改的部分, -写时"。)


我不知道这对您是否有帮助,但是当我不得不在一个项目中做类似的事情时,我最终从http://www.undomadeeasy.com下载了UndoEngine-一个很棒的引擎而且我真的不太在乎引擎盖上的东西-它只是起作用。


我认为,UNDO / REDO可以通过两种方式广泛实施。
1.命令级别(称为命令级别撤消/重做)
2.文档级别(称为全局撤消/重做)

命令级别:正如许多答案所指出的那样,这是使用Memento模式有效实现的。如果该命令还支持记录操作,则很容易支持重做。

限制:一旦超出命令范围,就无法进行撤消/重做,从而导致文档级(全局)撤消/重做

我猜您的情况适合于全局撤消/重做,因为它适用于涉及大量内存空间的模型。同样,这也适合于选择性地撤消/重做。有两种原始类型

  • 所有内存撤消/重做
  • 对象级别撤消重做
  • 在"所有内存撤消/重做"中,整个内存被视为已连接的数据(例如树,列表或图形),并且该内存由应用程序而非操作系统管理。因此,如果在C ++中将new和delete运算符重载为包含更具体的结构,以有效地实现诸如a的操作。如果有任何节点被修改,b。保存和清除数据等,
    它的工作方式基本上是复制整个内存(假设内存分配已由应用程序使用高级算法优化和管理),然后将其存储在堆栈中。如果请求复制内存,则根据需要具有浅副本或深副本来复制树结构。仅对已修改的变量进行深拷贝。由于每个变量都是使用自定义分配分配的,因此应用程序拥有最终决定何时删除(如果需要)的权限。
    如果我们必须对"撤消/重做"进行分区,那么事情就变得非常有趣,我们需要以编程方式选择性地撤消/重做一组操作。在这种情况下,仅对那些新变量,已删除变量或已修改变量赋予一个标志,以便"撤消/重做"仅撤消/重做那些内存。
    如果我们需要在对象内部进行部分撤消/重做操作,事情将变得更加有趣。在这种情况下,将使用"访客模式"的新概念。它称为"对象级别撤消/重做"

  • 对象级撤消/重做:当调用撤消/重做通知时,每个对象都将执行流操作,其中,流媒体从对象获取已编程的旧数据/新数据。不受干扰的数据不受干扰。每个对象都会得到一个流媒体作为参数,并且在UNDo / Redo调用中,它流化/解流该对象的数据。
  • 1和2都可以使用诸如
    1. BeforeUndo()
    2. AfterUndo()
    3. BeforeRedo()
    4. AfterRedo()。这些方法必须在基本的"撤消/重做命令"(而不是上下文命令)中发布,以便所有对象也可以实现这些方法以获得特定的操作。

    一个好的策略是创建1和2的混合体。美丽之处在于,这些方法(1&2)本身都使用命令模式


    推荐阅读

      3500元超额值学生娱乐结构的优化配置

      3500元超额值学生娱乐结构的优化配置,,作为一个DIY的主流用户领域的学生,每个用户51学生攒机的高峰。因为学生用户没有稳定的收入来源,攒机

      论竞技比赛结构中的十种常见误区

      论竞技比赛结构中的十种常见误区,,近年来,在中国电子竞技的发展得到了迅速的发展。虽然网络游戏市场正在进一步扩大,但我们也看到CS和魔兽争

      未知软件异常应用程序错误解决方案

      未知软件异常应用程序错误解决方案,,我有很多朋友的电脑未知软件异常应用程序错误的问题,对于异常未知的软件异常中的应用一般提示(0xc00000

      小米手机安装的应用程序包在哪里

      小米手机安装的应用程序包在哪里,,问题: 下载小米手机系统的安装文件在哪里 答案uff1a 下载应用程序包位置:SD卡>下载。 (摘自:小米手册:米

      做模型电脑|电脑上做模型的软件

      做模型电脑|电脑上做模型的软件,,电脑上做模型的软件建模软件 中文名 ;sketchup渲染软件 主要用在建筑方案中:对建筑造型的推敲 现在G

      应用程序对象

      应用程序对象,,应用程序对象是一个应用程序级对象,用于在所有用户之间共享信息,并且在Web应用程序运行期间可以保存数据。 应用的性质: 方法

      取消撤消快捷键|取消的快捷键

      取消撤消快捷键|取消的快捷键,,1. 取消的快捷键取消键盘快捷键的方式如下:1、首先在“开始”中找到“计算机”。2、点击右键打开“管理”。

      1.设计模式概述

      1.设计模式概述,模式,原则,[toc]1.为什么要使用设计模式使用设计模式可以重构整体架构代码、提交代码复用性、扩展性、减少代码冗余问题。