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

我已经编写了一个撤消引擎,该引擎在每次修改模型后都会保存深层副本。现在我开始思考是否可以采用不同的编码方式。我也许可以保存每个修改器操作的列表以及相应的反向修改器,而不是保存深层副本。这样我就可以将反向修改器应用于当前模型以进行撤消,或将修改器应用于重做。

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

人们将如何实施这一目标?

有帮助吗?

解决方案

我见过的大多数例子都使用了 命令模式 为了这。每个可撤消的用户操作都会获得自己的命令实例,其中包含执行该操作并将其回滚的所有信息。然后,您可以维护所有已执行命令的列表,并且可以将它们一一回滚。

其他提示

我认为当你处理OP暗示的大小和范围的模型时,纪念品和命令都不实用。它们可以工作,但维护和扩展需要大量工作。

对于此类问题,我认为您需要构建对数据模型的支持以支持差异检查点 每个物体 参与模型。我已经这样做过一次并且效果非常好。您要做的最重要的事情是避免在模型中直接使用指针或引用。

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

实现撤消/重做很简单:采取行动并建立新的检查点;将所有对象版本回滚到之前的检查点。

它需要一些代码纪律,但有很多优点:您不需要深层复制,因为您正在对模型状态进行差异存储;您可以调整要使用的内存量(非常 对于 CAD 模型等很重要),取决于重做次数或使用的内存;对于在模型上运行的函数来说,具有非常高的可扩展性和低维护性,因为它们不需要执行任何操作来实现撤消/重做。

如果你说的是 GoF, 纪念 模式专门解决撤消。

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

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

这里的关键是您可以使用撤消/重做系统作为编辑器的主要命令系统。您可以“创建undo对象,对undo对象执行重做操作来修改文档”,而不是编写诸如“创建undo对象,修改文档”之类的系统。

现在,诚然,许多人在思考“好吧,不是指挥模式的一部分吗?”是的,但是我看到了太多的命令系统,这些系统具有两组命令,一个用于即时操作,另一组用于撤消/重做。我并不是说不会有特定于立即操作和撤消/重做的命令,但减少重复将使代码更易于维护。

您可能想参考 Paint.NET 代码 对于他们的撤销 - 他们有一个非常好的撤销系统。它可能比您需要的要简单一些,但它可能会给您一些想法和指导。

-亚当

这可能是一种情况 中西医结合协会 是适用的。它旨在为 Windows 窗体应用程序中的对象提供复杂的撤消支持。

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

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

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

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

许多撤消系统仅位于内存中,但我想,如果您愿意,您可以将撤消堆栈保留下来。

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

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

更多信息 这里

我和 门德尔特·西本加 事实上,您应该使用命令模式。您使用的模式是纪念品模式,随着时间的推移,它可能而且将会变得非常浪费。

由于您正在开发内存密集型应用程序,因此您应该能够指定允许撤消引擎占用多少内存、保存多少级别的撤消或将其保留的某些存储。如果不这样做,您很快就会遇到由于机器内存不足而导致的错误。

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

Codeplex项目:

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

我读过的大多数示例都是通过使用命令或备忘录模式来完成的。但是你也可以在没有设计模式的情况下通过简单的 双端队列结构.

处理撤消的一种巧妙方法是实现一个,这将使您的软件也适合多用户协作 运营转型 的数据结构。

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

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

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

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

某些操作可能是速度/资源使用的特殊情况 - 例如调整事物的大小、移动事物。

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

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

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

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

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

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

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

您可以将最初的想法付诸实践。

使用 持久数据结构, ,并坚持保留 对周围旧状态的引用列表. 。(但是,只有当状态类中的所有数据操作都是不可变的,并且对它的所有操作都返回一个新版本时,这才真正有效——但是新版本不需要是深层复制,只需将更改的部分替换为“复制” -写入时'。)

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

如果您想要较低的实现工作量和易于维护性(并且可以为第二个实例提供额外的内存),那么这种方法似乎是合理的。

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

我不知道这对你是否有任何用处,但是当我不得不在我的一个项目中做类似的事情时,我最终从下载了 UndoEngine http://www.undomadeeasy.com - 一个很棒的引擎,我真的不太关心引擎盖下的东西 - 它只是工作。

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

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

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

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

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

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

  1. 对象级撤消/重做:当撤销/重做通知被调用时,每个对象实现流操作,其中流器从对象获取编程的旧数据/新数据。未受干扰的数据保持不变。每个对象都有一个流作为参数,并且在 UNDo/Redo 调用中,它流/取消流该对象的数据。

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

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top