撤消引擎的设计模式


117

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

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

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

如何实施呢?


如果添加注释“ Undo Algorthim”,它将使它可以搜索“ Undo Algorithm”并找到它?这就是我要搜索的内容,我发现某些内容已作为重复项关闭。
彼得·特纳

hay,我也想在正在开发的应用程序中开发撤消/重做。我们使用QT4框架,并且需要执行许多复杂的撤消/重做操作。
Ashika Umanga Umagiliya,2010年

2
@umanga:可行,但并不容易。最困难的部分是跟踪引用。例如,当删除Frame对象时,需要保留其子对象:节点,作用于其上的负载以及许多其他用户分配,以便在撤消时重新插入。但是其中一些子对象与其他对象共享,并且撤消/重做逻辑变得非常复杂。如果模型不是那么大,我将保留纪念品方法。实施起来容易得多。
Ozgur Ozcitak 2010年

这是一个很有趣的问题,考虑一下像svn这样的源代码存储库是如何做到的(它们保持提交之间的差异)。
亚历克斯(Alex)

Answers:


88

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


4
基本上,这就是Cocoa中的撤消引擎NSUndoManager的工作方式。
amrox

33

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

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

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

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

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


1
如果您使用数据库(例如sqlite)作为文件格式,这几乎是自动的
Martin Beckett

4
如果通过跟踪模型更改引入的依赖关系来增强此功能,则可能具有撤消树系统(即,如果我改变了大梁的宽度,然后在单独的组件上做一些工作,我可以回来撤消操作)大梁发生变化而不会丢失其他东西)。UI可能有点笨拙,但它比传统的线性撤消功能要强大得多。
Sumudu Fernando 2012年

您能进一步解释一下该ID的vs指针的想法吗?指针/内存地址肯定和id一样工作吗?
保罗

@paulm:本质上,实际数据由(id,版本)索引。指针指的是对象的特定版本,但您试图引用的是对象的当前状态,无论状态如何,因此您想通过id而不是(id,version)来寻址它。您可以对其进行重组,以便存储指向(version => data)表的指针,并且每次都选择最新的指针,但是当您持久存储数据时,这往往会损害局部性,造成一些麻烦,并使其更难进行某种常见的查询,因此通常无法做到这一点。
克里斯·摩根

17

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


7
并非如此,这解决了他的最初方法。他正在寻求替代方法。最初是为每个步骤存储完整状态,而后者仅存储“差异”。
AndreiRînea10年

15

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

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

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

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


1
我从未想过pastecut^ -1。
Lenar Hoyt



6

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

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

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

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

许多撤消系统仅是内存中的,但是我想,如果您愿意,可以保留撤消堆栈。


5

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

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

更多信息在这里


4

我在孟德尔·西本加(Mendelt Siebenga),您应该使用命令模式。您使用的模式是Memento模式,随着时间的流逝,它会而且非常浪费。

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

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


3

Codeplex项目

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


2

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


您会在双端队列中放入什么?

就我而言,我将要撤消/重做功能的操作的当前状态。通过具有两个双端队列(撤消/重做),我对撤消队列(弹出第一项)进行撤消,并将其插入到重做出队中。如果出队中的项目数超过了首选大小,我会弹出一条尾巴。
Patrik Svensson,2009年

2
您描述的实际上一种设计模式:)。这种方法的问题是,当您的状态占用大量内存时-保持几十个状态版本将变得不切实际甚至无法实现。
Igor Brejc,2009年

或者,您可以存储代表正常和撤消操作的闭包对。
阿康卡

2

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

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



1

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

由于指向对象的指针(C ++)在执行一些奇怪的撤消重做序列(那些位置未更新为更安全的撤消感知“标识符”)时从未修复的指针,因此存在许多错误。这个区域的错误通常...嗯...有趣。

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

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


随着模型大小的增加,这听起来越来越不可行。
沃伦·P

用什么方式?随着将新的“事物”添加到每个对象,该方法可以保持不变。随着对象序列化形式的增大,性能可能会成为一个问题-但这并不是主要问题。该系统已经持续开发20多年,并已被成千上万的用户使用。
Aardvark 2010年

1

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



0

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


0

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


0

您可以使您的最初创意生效。

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


0

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

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

参见此处的示例:https : //github.com/thilo20/Undo/


-1

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


仅当您有信心提供解决方案时,才将您的评论作为答案发表!否则,最好将其发布为问题下的评论!(如果现在不允许这样做!请
耐心

-1

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

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

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

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

  1. 所有内存撤消/重做
  2. 对象级别撤消重做

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

  1. 对象级撤消/重做:调用撤消/重做通知时,每个对象都将执行流操作,其中,流媒体程序从对象中获取已编程的旧数据/新数据。不受干扰的数据不受干扰。每个对象都会得到一个流媒体作为参数,并且在UNDo / Redo调用中,它将流媒体/解流该对象的数据。

1和2都可以具有诸如1的方法。BeforeUndo()2. AfterUndo()3. BeforeRedo()4. AfterRedo()。这些方法必须在基本的“撤消/重做命令”(而不是上下文命令)中发布,以便所有对象也可以实现这些方法以获得特定的操作。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.