撤销引擎的devise模式
我正在为民用工程应用程序编写一个结构build模工具。 我有一个巨大的模型类代表整个build筑物,其中包括节点集合,线元素,负载等也是自定义类。
我已经编写了一个撤消引擎,在每次修改模型之后都会保存一个深层拷贝。 现在我开始思考,如果我可以有不同的编码。 我可以用一个对应的反向修饰符来保存每个修饰符动作的列表,而不是保存这个深度拷贝。 这样我就可以将反向修饰符应用到当前模型来撤销,或修改器重做。
我可以想象你将如何执行改变对象属性的简单命令等等。但是复杂的命令呢? 就像向模型中插入新节点对象并添加一些保持对新节点的引用的线对象一样。
怎么去实现呢?
我见过的大多数例子都使用了Command模式的变体。 每个可撤销的用户操作都会获得自己的命令实例,并包含所有信息来执行操作并将其回滚。 然后,您可以维护已执行的所有命令的列表,并且可以逐个将它们回滚。
我认为,在处理OP意味着的规模和范围的模型时,纪念和命令都是不实际的。 他们会工作,但维护和延伸将是很多工作。
对于这类问题,我认为您需要build立对数据模型的支持,以支持模型中涉及的每个对象的差异检查点。 我做了一次,它的工作非常光滑。 你必须做的最大的事情是避免直接使用模型中的指针或引用。
每个对另一个对象的引用都使用一些标识符(如整数)。 无论何时需要对象,都可以从表中查找对象的当前定义。 该表包含每个包含所有先前版本的对象的链接列表,以及有关哪个检查点处于活动状态的信息。
实现撤销/重做很简单:做你的行动,并build立一个新的检查点; 将所有对象版本回滚到以前的检查点。
在代码中需要一些纪律,但是有很多优点:因为您正在进行模型状态的差异存储,所以不需要深度复制; 你可以通过使用的重做或者内存的数量来确定你想要使用的内存的数量(对于CAD模型来说非常重要) 对于在模型上运行的函数非常具有可扩展性和低维护性,因为他们不需要做任何事情来实现撤销/重做。
如果你正在谈论GoF, Memento模式专门解决撤消。
正如其他人所说,命令模式是一个非常强大的方法来执行撤消/重做。 但是我想提一下命令模式有一个重要的优势。
当使用命令模式实现撤销/重做时,可以通过抽象(一定程度上)对数据执行的操作来避免大量的重复代码,并在撤销/重做系统中使用这些操作。 例如,在文本编辑器中,剪切和粘贴是互补的命令(除了剪贴板的pipe理之外)。 换句话说,剪切的撤销操作是粘贴的,并且粘贴的撤消操作被剪切。 这适用于键入和删除文本更简单的操作。
这里的关键是你可以使用撤销/重做系统作为编辑器的主要命令系统。 可以不用写“创build撤销对象,修改文档”这样的系统,而是“创build撤销对象,对撤消对象执行重做操作来修改文档”。
可以肯定的是,很多人都在想自己:“呃,这不是指挥模式的一部分吗? 是的,但是我看到太多的命令系统有两组命令,一组用于立即操作,另一组用于撤销/重做。 我并不是说没有特定于立即操作和撤消/重做的命令,但减less重复将使代码更易于维护。
你可能想引用Paint.NET代码来取消它们 – 它们有一个非常好的撤销系统。 这可能比你想要的要简单一些,但是它可能会给你一些想法和指导。
-亚当
这可能是CSLA适用的情况。 它旨在为Windows窗体应用程序中的对象提供复杂的撤销支持。
我使用Memento模式成功地实现了复杂的撤消系统 – 非常简单,并且自然也提供了一个重做框架。 一个更微妙的好处是聚合动作可以包含在一个单一的撤消。
简而言之,你有两堆纪念物。 一个用于撤消,另一个用于重做。 每一个操作创build一个新的纪念品,理想情况下将是一些电话来改变你的模型,文件(或其他)的状态。 这被添加到撤消堆栈。 当您执行撤消操作时,除了对Memento对象执行Undo操作以再次更改模型之外,还可以将对象从撤消堆栈中popup,并将其右移到重做堆栈上。
如何实现更改文档状态的方法完全取决于您的实现。 如果你可以简单地做一个API调用(例如ChangeColour(r,g,b)),那么在它前面加上一个查询来获取和保存相应的状态。 但是这个模式还可以支持深拷贝,内存快照,临时文件创build等 – 这一切都取决于你,因为它只是一个虚拟的方法实现。
要执行聚合操作(例如,用户Shift-select要执行操作的对象的负载,例如删除,重命名,更改属性),代码将创build一个新的撤消堆栈作为单个logging,并将其传递给实际操作将单个操作添加到。 所以你的动作方法不需要(a)有一个全局堆栈可以担心和(b)可以被编码为相同的,无论它们是被孤立地执行还是作为一个集合操作的一部分被执行。
很多撤销系统只是内存,但是如果你愿意的话,你可以坚持撤销系统。
刚刚阅读了我的敏捷开发书中的命令模式 – 也许这有潜力?
你可以让每个命令实现命令接口(它有一个Execute()方法)。 如果你想撤消,你可以添加一个撤消方法。
更多信息在这里
我和Mendelt Siebenga谈到你应该使用命令模式。 你使用的模式是纪念模式,随着时间的推移,它可能会变得非常浪费。
由于您正在处理内存密集型应用程序,因此应该能够指定允许撤消引擎占用多less内存,保存多less级别的撤消或将其保留的某个存储。 如果你不这样做,你很快就会面临由于机器内存不足而导致的错误。
我会build议你检查是否有一个框架已经在您select的编程语言/框架中创build了一个undos的模型。 发明新的东西是很好的,但最好是在真实情况下写一些已经写好,debugging和testing的东西。 如果你添加了你正在写的东西,这将会有所帮助,所以人们可以推荐他们认识的框架。
我读过的大多数例子都是通过使用命令或者记忆模式来完成的。 但是你可以在没有devise模式的情况下用一个简单的双向结构来实现 。
作为参考,下面是C#中撤消/重做命令模式的简单实现:
http://www.catnapgames.com/blog/2009/03/19/simple-undo-redo-system-for-csharp.html
一个聪明的方法来处理撤消,这将使您的软件也适合多用户协作,正在实施数据结构的操作转换 。
这个概念不是很受欢迎,但很好定义和有用。 如果定义看起来太抽象了, 这个项目就是一个成功的例子,说明如何在Javascript中定义和实现JSON对象的操作转换
我们重新使用文件加载并保存“对象”的序列化代码,以便于保存和恢复对象的整个状态。 我们把这些序列化的对象放在撤销堆栈上 – 以及一些关于执行什么操作的信息,以及如果没有从序列化数据中收集到足够的信息,则提示撤消该操作。 撤销和重做通常只是将一个对象replace为另一个(理论上)。
当你执行一些奇怪的撤销重做序列(那些没有更新到更安全的撤销感知“标识符”的位置)时,由于指针(C ++)的存在,从来没有修复过的对象有很多很多的错误。 在这个领域的错误往往…嗯…有趣的。
有些操作可能是速度/资源使用的特殊情况 – 比如resize,移动东西。
多选也提供了一些有趣的复杂性。 幸运的是,我们已经在代码中有了一个分组的概念。 Kristopher Johnson对子项目的评论与我们所做的非常接近。
编写一个peg-jump益智游戏的解算器时,我必须这样做。 我做了每一个动作一个Command对象,它拥有足够的信息,可以完成或撤消。 就我而言,这就像存储每一步的起始位置和方向一样简单。 然后,我将所有这些对象存储在一个堆栈中,这样程序就可以很容易地在回溯时撤消尽可能多的移动。
我曾经在一个应用程序中使用命令对应用程序的模型(即CDocument …我们使用的MFC)所做的所有更改都被保留在命令的末尾,通过更新模型中维护的内部数据库中的字段。 所以我们不必为每个动作编写单独的撤销/重做代码。 每次更改logging(每个命令结束时),撤消堆栈都会记住主键,字段名称和旧值。
devise模式(GoF,1994)的第一部分有一个实现撤消/重做作为devise模式的用例。
在我看来,UNDO / REDO可以广泛地以两种方式实施。 1.命令级别(称为命令级别撤消/重做)2.文档级别(称为全局撤消/重做)
命令级别:许多答案指出,这是使用纪念图案有效地实现的。 如果该命令也支持logging操作,则轻松支持重做。
限制:一旦命令的作用域结束,撤销/重做是不可能的,这将导致文档级(全局)撤销/重做
我想你的情况会适合全局撤消/重做,因为它适合于涉及大量内存空间的模型。 而且,这也适用于有select地撤消/重做。 有两种基本types
- 所有的内存撤消/重做
- 对象级别撤消重做
在“所有内存撤消/重做”中,整个内存被视为连接数据(如树,或列表或graphics),内存由应用程序而不是操作系统pipe理。 因此,如果在C ++中新增和删除操作符被重载,以包含更多特定的结构来有效地实现诸如a的操作。 如果任何节点被修改,b。 保存和清除数据等,它的function基本上是复制整个内存(假设内存分配已经被应用程序使用高级algorithm优化和pipe理)并将其存储在一个堆栈中。 如果请求存储器的副本,则根据需要具有浅或深的副本来复制树结构。 只对被修改的variables进行深层复制。 由于每个variables都是使用自定义分配来分配的,因此如果需要,应用程序有最后的时间来删除它。 如果我们必须对Undo / Redo进行分区,事情就会变得非常有趣,因为我们需要以编程方式select性地撤销/重做一组操作。 在这种情况下,只有那些新variables或已删除的variables或已修改的variables才会被赋予一个标志,以便撤销/重做只会撤消/重做这些内存。如果我们需要在对象内部执行部分撤销/重做,情况就会变得更加有趣。 如果是这种情况,则使用“访问者模式”的新概念。 它被称为“对象级撤销/重做”
- 对象级别撤消/重做:当调用撤消/重做通知时,每个对象实现一个stream操作,其中stream转器从对象获得被编程的旧数据/新数据。 不受干扰的数据不受干扰。 每个对象都会得到一个stream媒体作为参数,并在UNDo / Redo调用中,将对象的数据进行stream式处理/取消stream式处理。
1和2都可以具有如下方法:1. BeforeUndo()2. AfterUndo()3. BeforeRedo()4. AfterRedo()。 这些方法必须在基本的Undo / redo命令(而不是上下文命令)中发布,以便所有对象都实现这些方法以获得特定的操作。
一个好的策略是创build1和2的混合体。美的是这些方法(1和2)本身使用命令模式
你可以使你的初始想法的性能。
使用持久化的数据结构 ,坚持保留一个旧状态的引用列表 。 (但是,只有在你的状态类中的所有数据都是不可变的,并且所有的操作都返回一个新的版本的时候,它才会真正起作用 – 但是新版本不需要是深层的拷贝,只需要replace被更改的部分的拷贝-on-写”。)
你可以在PostSharp中试试现成的Undo / Redo模式。 https://www.postsharp.net/model/undo-redo
它可以让你添加撤消/重做function到你的应用程序,而无需自己实现模式。 它使用可logging模式来跟踪模型中的变化,它可以与PostSharp中实现的INotifyPropertyChanged模式一起使用。
您提供了UI控件,您可以决定每个操作的名称和粒度。
我不知道这是否对你有任何用处,但是当我不得不在我的一个项目上做类似的事情时,我最终从http://www.undomadeeasy.com下载了UndoEngine – 一个美妙的引擎而且我真的不在乎引擎盖下的东西太多了 – 它刚刚起作用。