我正在为土木工程应用程序编写一个结构建模工具。 我有一个巨大的模型类代表整个建筑物,其中包括节点、线元素、荷载等的集合,它们也是自定义类。
我已经编写了一个撤消引擎,该引擎在每次修改模型后都会保存深层副本。 现在我开始思考是否可以采用不同的编码方式。 我也许可以保存每个修改器操作的列表以及相应的反向修改器,而不是保存深层副本。 这样我就可以将反向修改器应用于当前模型以进行撤消,或将修改器应用于重做。
我可以想象您将如何执行更改对象属性等的简单命令。但是复杂的命令又如何呢? 就像将新节点对象插入到模型中并添加一些保留对新节点的引用的线对象一样。
人们将如何实施这一目标?
I'm writing a structural modeling tool for a civil enginering application. I have one huge model class representing the entire building, which include collections of nodes, line elements, loads, etc. which are also custom classes.
I have already coded an undo engine which saves a deep-copy after each modification to the model. Now I started thinking if I could have coded differently. Instead of saving the deep-copies, I could perhaps save a list of each modifier action with a corresponding reverse modifier. So that I could apply the reverse modifiers to the current model to undo, or the modifiers to redo.
I can imagine how you would carry out simple commands that change object properties, etc. But how about complex commands? Like inserting new node objects to the model and adding some line objects which keep references to the new nodes.
How would one go about implementing that?
发布评论
评论(22)
刚刚在我的敏捷开发书中阅读了有关命令模式的内容 - 也许这有潜力?
您可以让每个命令都实现命令接口(它有一个 Execute() 方法)。 如果想要撤消,可以添加撤消方法。
更多信息此处
Just been reading about the command pattern in my agile development book - maybe that's got potential?
You can have every command implement the command interface (which has an Execute() method). If you want undo, you can add an Undo method.
more info here
如果您谈论的是 GoF,则 Memento 模式专门解决撤消问题。
If you're talking GoF, the Memento pattern specifically addresses undo.
正如其他人所说,命令模式是实现撤消/重做的一种非常强大的方法。 但我想提一下命令模式的一个重要优点。
当使用命令模式实现撤消/重做时,您可以通过抽象(在一定程度上)对数据执行的操作并在撤消/重做系统中利用这些操作来避免大量重复代码。 例如,在文本编辑器中,剪切和粘贴是互补的命令(除了剪贴板的管理之外)。 换句话说,剪切的撤消操作是粘贴,粘贴的撤消操作是剪切。 这适用于更简单的操作,例如键入和删除文本。
这里的关键是您可以使用撤消/重做系统作为编辑器的主要命令系统。 您可以“创建undo对象,对undo对象执行重做操作来修改文档”,而不是编写诸如“创建undo对象,修改文档”之类的系统。
现在,诚然,许多人都在想:“呃,这不是命令模式的重点吗?” 是的,但我见过太多的命令系统有两组命令,一组用于立即操作,另一组用于撤消/重做。 我并不是说不会有特定于立即操作和撤消/重做的命令,但减少重复将使代码更易于维护。
As others have stated, the command pattern is a very powerful method of implementing Undo/Redo. But there is important advantage I would like to mention to the command pattern.
When implementing undo/redo using the command pattern, you can avoid large amounts of duplicated code by abstracting (to a degree) the operations performed on the data and utilize those operations in the undo/redo system. For example in a text editor cut and paste are complementary commands (aside from the management of the clipboard). In other words, the undo operation for a cut is paste and the undo operation for a paste is cut. This applies to much simpler operations as typing and deleting text.
The key here is that you can use your undo/redo system as the primary command system for your editor. Instead of writing the system such as "create undo object, modify the document" you can "create undo object, execute redo operation on undo object to modify the document".
Now, admittedly, many people are thinking to themselves "Well duh, isn't part of the point of the command pattern?" Yes, but I've seen too many command systems that have two sets of commands, one for immediate operations and another set for undo/redo. I'm not saying that there won't be commands that are specific to immediate operations and undo/redo, but reducing the duplication will make the code more maintainable.
这可能是 CSLA 适用的情况。 它旨在为 Windows 窗体应用程序中的对象提供复杂的撤消支持。
This might be a case where CSLA is applicable. It was designed to provide complex undo support to objects in Windows Forms applications.
在我看来,UNDO/REDO 可以通过两种方式广泛地实现。
1.命令级(称为命令级Undo/Redo)
2.文档级别(称为全局撤消/重做)
命令级别:正如许多答案指出的那样,这是使用备忘录模式有效实现的。 如果该命令还支持记录操作,则很容易支持重做。
限制:一旦命令的范围超出,撤消/重做是不可能的,这会导致文档级别(全局)撤消/重做
我猜你的情况适合全局撤消/重做,因为它适合涉及的模型大量的内存空间。 此外,这也适合选择性地撤消/重做。 有两种基本类型
在“全内存撤消/重做”中,整个内存被视为连接的数据(例如树,或列表或图形),并对内存进行管理由应用程序而不是操作系统。 因此,在 C++ 中,new 和 delete 运算符被重载以包含更具体的结构,以有效地实现诸如 a 之类的操作。 如果任何节点被修改,b. 保存和清除数据等,
它的功能基本上是复制整个内存(假设内存分配已经由应用程序使用高级算法优化和管理)并将其存储在堆栈中。 如果请求内存复制,则根据浅复制或深复制的需要来复制树结构。 仅针对修改的变量进行深层复制。 由于每个变量都是使用自定义分配来分配的,因此应用程序拥有在需要时删除它的最终决定权。
当我们需要以编程方式选择性地撤消/重做一组操作时,如果我们必须对撤消/重做进行分区,事情就会变得非常有趣。 在这种情况下,只有那些新变量,或删除的变量或修改的变量被赋予一个标志,以便撤消/重做仅撤消/重做这些内存
如果我们需要在对象内进行部分撤消/重做,事情会变得更加有趣。 在这种情况下,将使用“访问者模式”的新概念。 它被称为“对象级撤消/重做”
1 和 2 都可以有这样的方法
1.BeforeUndo()
2.撤销后()
3.BeforeRedo()
4.AfterRedo()。 这些方法必须在基本撤消/重做命令(而不是上下文命令)中发布,以便所有对象也实现这些方法以获得特定操作。
一个好的策略是创建 1 和 2 的混合体。美妙之处在于这些方法(1 和 2)本身使用命令模式
In my opinion, the UNDO/REDO could be implemented in 2 ways broadly.
1. Command Level (called command level Undo/Redo)
2. Document level (called global Undo/Redo)
Command level: As many answers point out, this is efficiently achieved using Memento pattern. If the command also supports journalizing the action, a redo is easily supported.
Limitation: Once the scope of the command is out, the undo/redo is impossible, which leads to document level(global) undo/redo
I guess your case would fit into the global undo/redo since it is suitable for a model which involves a lot of memory space. Also, this is suitable to selectively undo/redo also. There are two primitive types
In "All memory Undo/Redo", the entire memory is treated as a connected data (such as a tree, or a list or a graph) and the memory is managed by the application rather than the OS. So new and delete operators if in C++ are overloaded to contain more specific structures to effectively implement operations such as a. If any node is modified, b. holding and clearing data etc.,
The way it functions is basically to copy the entire memory(assuming that memory allocation is already optimized and managed by the application using advanced algorithms) and store it in a stack. If the copy of the memory is requested, the tree structure is copied based on the need to have a shallow or deep copy. A deep copy is made only for that variable which is modified. Since every variable is allocated using custom allocation, the application has the final say when to delete it if need be.
Things become very interesting if we have to partition the Undo/Redo when it so happens that we need to programatically-selectively Undo/Redo a set of operation. In this case, only those new variables, or deleted variables or modified variables are given a flag so that Undo/Redo only undoes/redoes those memory
Things become even more interesting if we need to do a partial Undo/Redo inside an object. When such is the case, a newer idea of "Visitor pattern" is used. It is called "Object Level Undo/redo"
Both 1 and 2 could have methods such as
1. BeforeUndo()
2. AfterUndo()
3. BeforeRedo()
4. AfterRedo(). These methods have to be published in the basic Undo/redo Command ( not the contextual command) so that all objects implement these methods too to get specific action.
A good strategy is to create a hybrid of 1 and 2. The beauty is that these methods(1&2) themselves use command patterns
我已经使用 Memento 模式成功实现了复杂的撤消系统 - 非常简单,并且还具有自然提供重做框架的好处。 一个更微妙的好处是聚合操作也可以包含在单个撤消中。
简而言之,您有两堆纪念品。 一个用于撤消,另一个用于重做。 每个操作都会创建一个新的备忘录,理想情况下是一些更改模型、文档(或其他内容)状态的调用。 这将被添加到撤消堆栈中。 当您执行撤消操作时,除了对 Memento 对象执行撤消操作以再次将模型更改回来之外,您还可以将该对象从撤消堆栈中弹出并将其直接推入重做堆栈。
如何实现更改文档状态的方法完全取决于您的实现。 如果您可以简单地进行 API 调用(例如 ChangeColour(r,g,b)),则在其前面添加查询以获取并保存相应的状态。 但该模式还将支持深度复制、内存快照、临时文件创建等 - 这一切都取决于您,因为它只是一个虚拟方法实现。
要执行聚合操作(例如,用户 Shift-选择要执行操作的对象负载,例如删除、重命名、更改属性),您的代码将创建一个新的撤消堆栈作为单个备忘录,并将其传递给实际操作将各个操作添加到。 因此,您的操作方法不需要 (a) 需要担心全局堆栈,并且 (b) 无论它们是单独执行还是作为一个聚合操作的一部分执行,都可以进行相同的编码。
许多撤消系统仅位于内存中,但我想,如果您愿意,您可以将撤消堆栈保留下来。
I've implemented complex undo systems sucessfully using the Memento pattern - very easy, and has the benefit of naturally providing a Redo framework too. A more subtle benefit is that aggregate actions can be contained within a single Undo too.
In a nutshell, you have two stacks of memento objects. One for Undo, the other for Redo. Every operation creates a new memento, which ideally will be some calls to change the state of your model, document (or whatever). This gets added to the undo stack. When you do an undo operation, in addition to executing the Undo action on the Memento object to change the model back again, you also pop the object off the Undo stack and push it right onto the Redo stack.
How the method to change the state of your document is implemented depends completely on your implementation. If you can simply make an API call (e.g. ChangeColour(r,g,b)), then precede it with a query to get and save the corresponding state. But the pattern will also support making deep copies, memory snapshots, temp file creation etc - it's all up to you as it is is simply a virtual method implementation.
To do aggregate actions (e.g. user Shift-Selects a load of objects to do an operation on, such as delete, rename, change attribute), your code creates a new Undo stack as a single memento, and passes that to the actual operation to add the individual operations to. So your action methods don't need to (a) have a global stack to worry about and (b) can be coded the same whether they are executed in isolation or as part of one aggregate operation.
Many undo systems are in-memory only, but you could persist the undo stack out if you wish, I guess.
我见过的大多数示例都使用 Command-Pattern 的变体来实现此目的。 每个可撤消的用户操作都会获得自己的命令实例,其中包含执行该操作并将其回滚的所有信息。 然后,您可以维护所有已执行命令的列表,并且可以将它们一一回滚。
Most examples I've seen use a variant of the Command-Pattern for this. Every user-action that's undoable gets its own command instance with all the information to execute the action and roll it back. You can then maintain a list of all the commands that have been executed and you can roll them back one by one.
您可以尝试 PostSharp 中现成的撤消/重做模式实现。 https://www.postsharp.net/model/undo-redo
它可以让您添加为您的应用程序提供撤消/重做功能,而无需您自己实现该模式。 它使用 Recordable 模式来跟踪模型中的更改,并与 INotifyPropertyChanged 模式配合使用,该模式也在 PostSharp 中实现。
我们为您提供了 UI 控件,您可以决定每个操作的名称和粒度。
You can try ready-made implementation of Undo/Redo pattern in PostSharp. https://www.postsharp.net/model/undo-redo
It lets you add undo/redo functionality to your application without implementing the pattern yourself. It uses Recordable pattern to track the changes in your model and it works with INotifyPropertyChanged pattern which is also implemented in PostSharp.
You are provided with UI controls and you can decide what the name and granularity of each operation will be.
您可以将最初的想法付诸实践。
使用持久数据结构,并坚持保留对旧状态的引用列表。 (但是,只有当状态类中的所有数据操作都是不可变的,并且对它的所有操作都返回一个新版本时,这才真正有效——但是新版本不需要是深层复制,只需将更改的部分替换为“复制” -写入时'。)
You can make your initial idea performant.
Use persistent data structures, and stick with keeping a list of references to old state around. (But that only really works if operations all data in your state class are immutable, and all operations on it return a new version---but the new version doesn't need to be a deep copy, just replace the changed parts 'copy-on-write'.)
我曾经开发过一个应用程序,其中命令对应用程序模型(即 CDocument...我们使用的是 MFC)所做的所有更改都通过更新模型中维护的内部数据库中的字段来保留在命令末尾。 因此,我们不必为每个操作编写单独的撤消/重做代码。 每次更改记录时(在每个命令的末尾),撤消堆栈都会记住主键、字段名称和旧值。
I once worked on an application in which all changes made by a command to the application's model (i.e. CDocument... we were using MFC) were persisted at the end of the command by updating fields in an internal database maintained within the model. So we did not have to write separate undo/redo code for each action. The undo stack simply remembered the primary keys, field names and old values every time a record was changed (at the end of each command).
设计模式(GoF,1994)的第一部分有一个将撤消/重做实现为设计模式的用例。
The first section of Design Patterns (GoF, 1994) has a use case for implementing the undo/redo as a design pattern.
我发现命令模式在这里非常有用。 我没有实现多个反向命令,而是在 API 的第二个实例上使用回滚并延迟执行。
如果您想要较低的实现工作量和易于维护性(并且可以为第二个实例提供额外的内存),那么这种方法似乎是合理的。
请参阅此处的示例:
https://github.com/thilo20/Undo/
I've found the Command pattern to be very useful here. Instead of implementing several reverse commands, I'm using rollback with delayed execution on a second instance of my API.
This approach seems reasonable if you want low implementation effort and easy maintainability (and can afford the extra memory for the 2nd instance).
See here for an example:
https://github.com/thilo20/Undo/
我不知道这对你是否有任何用处,但是当我不得不在我的一个项目中做类似的事情时,我最终从 http://www.undomadeeasy.com - 一个很棒的引擎,我真的不太关心引擎盖下的内容 - 它只是工作。
I don't know if this is going to be of any use to you, but when I had to do something similar on one of my projects, I ended up downloading UndoEngine from http://www.undomadeeasy.com - a wonderful engine and I really didn't care too much about what was under the bonnet - it just worked.
我们重用了“对象”的文件加载和保存序列化代码,以方便的形式保存和恢复对象的整个状态。 我们将这些序列化对象推送到撤消堆栈上,以及有关执行的操作的一些信息,以及在没有从序列化数据中收集到足够信息时撤消该操作的提示。 撤消和重做通常只是用一个对象替换另一个对象(理论上)。
由于指向对象的指针(C++)在执行一些奇怪的撤消重做序列时从未修复(这些位置未更新为更安全的撤消感知“标识符”),因此存在许多错误。 这个领域的错误经常......嗯......有趣。
某些操作可能是速度/资源使用的特殊情况 - 例如调整事物的大小、移动事物。
多重选择也提供了一些有趣的复杂性。 幸运的是,我们在代码中已经有了分组的概念。 克里斯托弗·约翰逊(Kristopher Johnson)对分项的评论与我们的评论非常接近。
We reused the file load and save serialization code for “objects” for a convenient form to save and restore the entire state of an object. We push those serialized objects on the undo stack – along with some information about what operation was performed and hints on undo-ing that operation if there isn’t enough info gleaned from the serialized data. Undo and Redoing is often just replacing one object with another (in theory).
There have been many MANY bugs due to pointers (C++) to objects that were never fixed-up as you perform some odd undo redo sequences (those places not updated to safer undo aware “identifiers”). Bugs in this area often ...ummm... interesting.
Some operations can be special cases for speed/resource usage - like sizing things, moving things around.
Multi-selection provides some interesting complications as well. Luckly we already had a grouping concept in the code. Kristopher Johnson comment about sub-items is pretty close to what we do.
处理撤消的一种巧妙方法是实施操作转换<,这将使您的软件也适合多用户协作/a> 数据结构。
这个概念不是很流行,但定义明确且有用。 如果这个定义对您来说太抽象,这个项目是一个成功的示例,展示了如何对 JSON 对象进行操作转换在Javascript中定义和实现
A clever way to handle undo, which would make your software also suitable for multi user collaboration, is implementing an operational transformation of the data structure.
This concept is not very popular but well defined and useful. If the definition looks too abstract to you, this project is a successful example of how an operational transformation for JSON objects is defined and implemented in Javascript
当我为一个跳跃益智游戏编写解算器时,我必须这样做。 我为每个移动创建了一个 Command 对象,该对象包含足够的信息,可以完成或撤消该移动。 就我而言,这就像存储起始位置和每次移动的方向一样简单。 然后,我将所有这些对象存储在堆栈中,以便程序可以在回溯时轻松撤消所需的任意数量的移动。
I had to do this when writing a solver for a peg-jump puzzle game. I made each move a Command object that held enough information that it could be either done or undone. In my case this was as simple as storing the starting position and the direction of each move. I then stored all these objects in a stack so the program could easily undo as many moves as it needed while backtracking.
我读过的大多数示例都是通过使用命令或备忘录模式来完成的。 但是您也可以在没有设计模式的情况下使用简单的 deque-struct 来完成此操作。
Most examples I've read do it by using either the command or memento pattern. But you can do it without design patterns too with a simple deque-structure.
作为参考,以下是 C# 中撤消/重做命令模式的简单实现: C# 的简单撤消/重做系统。
For reference, here's a simple implementation of the Command pattern for Undo/Redo in C#: Simple undo/redo system for C#.
Codeplex 项目:
这是一个简单的框架,基于经典的命令设计模式,可以将撤消/重做功能添加到您的应用程序中。 它支持合并操作、嵌套事务、延迟执行(在顶级事务提交上执行)和可能的非线性撤消历史记录(您可以选择多个操作来重做)。
Codeplex project:
It's a simple framework to add Undo/Redo functionality to your applications, based on the classical Command design pattern. It supports merging actions, nested transactions, delayed execution (execution on top-level transaction commit) and possible non-linear undo history (where you can have a choice of multiple actions to redo).
我同意 Mendelt Siebenga 你应该使用命令模式。 您使用的模式是纪念品模式,随着时间的推移,它可能而且将会变得非常浪费。
由于您正在开发内存密集型应用程序,因此您应该能够指定允许撤消引擎占用多少内存、保存多少级别的撤消或将其保留的某些存储。 如果不这样做,您很快就会遇到由于机器内存不足而导致的错误。
我建议您检查是否有一个框架已经在您选择的编程语言/框架中创建了撤消模型。 发明新东西固然很好,但最好还是采用已经在实际场景中编写、调试和测试的东西。 如果您添加正在编写的内容,将会有所帮助,这样人们就可以推荐他们知道的框架。
I'm with Mendelt Siebenga on the fact that you should use the Command Pattern. The pattern you used was the Memento Pattern, which can and will become very wasteful over time.
Since you're working on a memory-intensive application, you should be able to specify either how much memory the undo engine is allowed to take up, how many levels of undo are saved or some storage to which they will be persisted. Should you not do this, you will soon face errors resulting from the machine being out of memory.
I would advise you check whether there's a framework that already created a model for undos in the programming language / framework of your choice. It is nice to invent new stuff, but it's better to take something already written, debugged and tested in real scenarios. It would help if you added what you're writing this in, so people can recommend frameworks they know.
您可能需要参考 Paint.NET 代码 进行撤消 - 他们有一个非常好的撤消系统。 它可能比您需要的要简单一些,但它可能会给您一些想法和指导。
-亚当
You might want to refer to the Paint.NET code for their undo - they've got a really nice undo system. It's probably a bit simpler than what you'll need, but it might give you some ideas and guidelines.
-Adam
我认为当你处理OP暗示的大小和范围的模型时,纪念品和命令都不实用。 它们可以工作,但维护和扩展需要大量工作。
对于此类问题,我认为您需要构建对数据模型的支持,以支持模型中涉及的每个对象的差异检查点。 我已经这样做过一次并且效果非常好。 您要做的最重要的事情是避免在模型中直接使用指针或引用。
对另一个对象的每个引用都使用一些标识符(如整数)。 每当需要该对象时,您都可以从表中查找该对象的当前定义。 该表包含每个对象的链接列表,其中包含所有先前版本,以及有关它们在哪个检查点处于活动状态的信息。
实施撤消/重做很简单:执行您的操作并建立一个新的检查点; 将所有对象版本回滚到之前的检查点。
它需要一些代码纪律,但有很多优点:您不需要深度复制,因为您正在对模型状态进行差异存储; 您可以通过重做次数或使用的内存来确定要使用的内存量(对于 CAD 模型等非常); 对于在模型上运行的函数来说,具有非常高的可扩展性和低维护性,因为它们不需要执行任何操作来实现撤消/重做。
I think both memento and command are not practical when you are dealing with a model of the size and scope that the OP implies. They would work, but it would be a lot of work to maintain and extend.
For this type of problem, I think you need to build in support to your data model to support differential checkpoints for every object involved in the model. I've done this once and it worked very slick. The biggest thing you have to do is avoid the direct use of pointers or references in the model.
Every reference to another object uses some identifier (like an integer). Whenever the object is needed, you lookup the current definition of the object from a table. The table contains a linked list for each object that contains all the previous versions, along with information regarding which checkpoint they were active for.
Implementing undo/redo is simple: Do your action and establish a new checkpoint; rollback all object versions to the previous checkpoint.
It takes some discipline in the code, but has many advantages: you don't need deep copies since you are doing differential storage of the model state; you can scope the amount of memory you want to use (very important for things like CAD models) by either number of redos or memory used; very scalable and low-maintenance for the functions that operate on the model since they don't need to do anything to implement undo/redo.