There are two classic patterns to use. The first is the memento pattern which is used to store snapshots of your complete object state. This is perhaps more system intensive than the command pattern, but it allows rollback very simply to an older snapshot. You could store the snapshots on disk a la PaintShop/PhotoShop or keep them in memory for smaller objects that don't require persistence. What you're doing is exactly what this pattern was designed for, so it should fit the bill slightly better than the Command Pattern suggested by others.
Also, an additional note is that because it doesn't require you to have reciprocal commands to undo something that was previously done, it means that any potentially one way functions [such as hashing or encryption] which can't be undone trivially using reciprocal commands can still be undone very simply by just rolling back to an older snapshot.
Also as pointed out, the command pattern which is potentially less resource intensive, so I will concede that in specific cases where:
There is a large object state to be persisted and/or
There are no destructive methods and
Where reciprocal commands can be used very trivially to reverse any action taken
the command pattern may be a better fit [but not necessarily, it will depend very much on the situation]. In other cases, I would use the memento pattern.
I would probably refrain from using a mashup of the two because I tend to care about the developer that's going to come in behind me and maintain my code as well as it being my ethical responsibility to my employer to make that process as simple and inexpensive as possible. I see a mashup of the two patterns easily becoming an unmaintainable rat hole of discomfort that would be expensive to maintain.
每个动作及其相反的动作都需要封装在一个对象中。如果您的行动不平凡,那么这可能会很困难。 (反向)操作中的错误确实很难调试,并且很容易导致致命的崩溃。即使看似简单的动作通常也涉及相当多的复杂性。例如,在 3D 编辑器的情况下,用于添加到模型的对象需要存储添加的内容、当前选择的颜色、覆盖的内容、镜像模式是否处于活动状态等。
There are three approaches here that are viable. Memento Pattern (Snapshots), Command Pattern and State Diffing. They all have advantages and disadvantages and it really comes down to your use case, what data you are working with and what you are willing to implement.
I would go with State Diffing if you can get away with it as it combines memory reduction with ease of implementation and maintainability.
I'm going to quote an article describing the three approaches (Reference below).
Below is an adapted excerpt from the article. However I do recommend that you read it in full.
Memento Pattern
Each history state stores a full copy. An action creates a new state and a pointer is used to move between the states to allow for undo and redo.
Pros
Implementation is independent of the applied action. Once implemented we can add actions without worrying about breaking history.
It is fast to advance to a predefined position in history. This is interesting when the actions applied between current and desired history position are computationally expensive.
Cons
Memory Requirements can be significantly higher compared to other approaches.
Loading time can be slow if the snapshots are large.
Command Pattern
Similar to the Memento Pattern, but instead of storing the full state, only the difference between the states is stored. The difference is stored as actions that can be applied and un-applied. When introducing a new action, apply and un-apply need to be implemented.
Pros
Memory footprint is small. We only need to store the changes to the model and if these are small, then the history stack is also small.
Cons
We can not go to an arbitrary position directly, but rather need to un-apply the history stack until we get there. This can be time consuming.
Every action and it's reverse needs to be encapsulated in an object. If your action is non trivial this can be difficult. Mistakes in the (reverse) action are really hard to debug and can easily result in fatal crashes. Even simple looking actions usually involve a good amount of complexity. E.g. in case of the 3D Editor, the object for adding to the model needs to store what was added, what color was currently selected, what was overwritten, if mirror mode active etc.
Can be challenging to implement and memory intensive when actions do not have a simple reverse, e.g when blurring an image.
State Diffing
Similar to the Command Pattern, but the difference is stored independent of the action by simply xor-nig the states. Introducing a new action does not require any special considerations.
Pros
Implementation is independent of the applied action. Once the history functionality is added we can add actions without worrying about breaking history.
Memory Requirements is usually much lower than for the Snapshot approach and in a lot of cases comparable to the Command Pattern approach. However this highly depends on the type of actions applied. E.g. inverting the color of an image using the Command Pattern should be very cheap, while State Diffing would save the whole image. Conversely when drawing a long free-form line, the Command Pattern approach might use more memory if it chained history entries for each pixel.
Cons / Limitations
We can not go to an arbitrary position directly, but rather need to un-apply the history stack until we get there.
We need to compute the diff between states. This can be expensive.
Implementing the xor diff between model states might be hard to implement depending on your data model.
You can encapsulate any object that performs an action with a command, and have it perform the reverse action with an Undo() method. You store all the actions in a stack for an easy way of rewinding through them.
public abstract class Actie
{
public Actie(Vorm[] Vormen)
{
vormen = Vormen;
}
private Vorm[] vormen = new Vorm[] { };
public Vorm[] Vormen
{
get { return vormen; }
}
public abstract void Undo();
public abstract void Redo();
}
用于添加形状的派生类:
public class VormenToegevoegdActie : Actie
{
public VormenToegevoegdActie(Vorm[] Vormen, Tekening tek)
: base(Vormen)
{
this.tek = tek;
}
private Tekening tek;
public override void Redo()
{
tek.Vormen.CanRaiseEvents = false;
tek.Vormen.AddRange(Vormen);
tek.Vormen.CanRaiseEvents = true;
}
public override void Undo()
{
tek.Vormen.CanRaiseEvents = false;
foreach(Vorm v in Vormen)
tek.Vormen.Remove(v);
tek.Vormen.CanRaiseEvents = true;
}
}
用于删除形状的派生类:
public class VormenVerwijderdActie : Actie
{
public VormenVerwijderdActie(Vorm[] Vormen, Tekening tek)
: base(Vormen)
{
this.tek = tek;
}
private Tekening tek;
public override void Redo()
{
tek.Vormen.CanRaiseEvents = false;
foreach(Vorm v in Vormen)
tek.Vormen.Remove(v);
tek.Vormen.CanRaiseEvents = true;
}
public override void Undo()
{
tek.Vormen.CanRaiseEvents = false;
foreach(Vorm v in Vormen)
tek.Vormen.Add(v);
tek.Vormen.CanRaiseEvents = true;
}
}
用于属性更改的派生类:
public class PropertyChangedActie : Actie
{
public PropertyChangedActie(Vorm[] Vormen, string PropertyName, object OldValue, object NewValue)
: base(Vormen)
{
propertyName = PropertyName;
oldValue = OldValue;
newValue = NewValue;
}
private object oldValue;
public object OldValue
{
get { return oldValue; }
}
private object newValue;
public object NewValue
{
get { return newValue; }
}
private string propertyName;
public string PropertyName
{
get { return propertyName; }
}
public override void Undo()
{
//Type t = base.Vorm.GetType();
PropertyInfo info = Vormen.First().GetType().GetProperty(propertyName);
foreach(Vorm v in Vormen)
{
v.CanRaiseVeranderdEvent = false;
info.SetValue(v, oldValue, null);
v.CanRaiseVeranderdEvent = true;
}
}
public override void Redo()
{
//Type t = base.Vorm.GetType();
PropertyInfo info = Vormen.First().GetType().GetProperty(propertyName);
foreach(Vorm v in Vormen)
{
v.CanRaiseVeranderdEvent = false;
info.SetValue(v, newValue, null);
v.CanRaiseVeranderdEvent = true;
}
}
}
每次 Vormen = 提交更改的项目数组。 它应该像这样使用:
堆栈声明:
Stack<Actie> UndoStack = new Stack<Actie>();
Stack<Actie> RedoStack = new Stack<Actie>();
I wrote a really flexible system to keep track of changes. I have a drawing program which implements 2 types of changes:
add/remove a shape
property change of a shape
Base class:
public abstract class Actie
{
public Actie(Vorm[] Vormen)
{
vormen = Vormen;
}
private Vorm[] vormen = new Vorm[] { };
public Vorm[] Vormen
{
get { return vormen; }
}
public abstract void Undo();
public abstract void Redo();
}
Derived class for adding shapes:
public class VormenToegevoegdActie : Actie
{
public VormenToegevoegdActie(Vorm[] Vormen, Tekening tek)
: base(Vormen)
{
this.tek = tek;
}
private Tekening tek;
public override void Redo()
{
tek.Vormen.CanRaiseEvents = false;
tek.Vormen.AddRange(Vormen);
tek.Vormen.CanRaiseEvents = true;
}
public override void Undo()
{
tek.Vormen.CanRaiseEvents = false;
foreach(Vorm v in Vormen)
tek.Vormen.Remove(v);
tek.Vormen.CanRaiseEvents = true;
}
}
Derived class for removing shapes:
public class VormenVerwijderdActie : Actie
{
public VormenVerwijderdActie(Vorm[] Vormen, Tekening tek)
: base(Vormen)
{
this.tek = tek;
}
private Tekening tek;
public override void Redo()
{
tek.Vormen.CanRaiseEvents = false;
foreach(Vorm v in Vormen)
tek.Vormen.Remove(v);
tek.Vormen.CanRaiseEvents = true;
}
public override void Undo()
{
tek.Vormen.CanRaiseEvents = false;
foreach(Vorm v in Vormen)
tek.Vormen.Add(v);
tek.Vormen.CanRaiseEvents = true;
}
}
Derived class for property changes:
public class PropertyChangedActie : Actie
{
public PropertyChangedActie(Vorm[] Vormen, string PropertyName, object OldValue, object NewValue)
: base(Vormen)
{
propertyName = PropertyName;
oldValue = OldValue;
newValue = NewValue;
}
private object oldValue;
public object OldValue
{
get { return oldValue; }
}
private object newValue;
public object NewValue
{
get { return newValue; }
}
private string propertyName;
public string PropertyName
{
get { return propertyName; }
}
public override void Undo()
{
//Type t = base.Vorm.GetType();
PropertyInfo info = Vormen.First().GetType().GetProperty(propertyName);
foreach(Vorm v in Vormen)
{
v.CanRaiseVeranderdEvent = false;
info.SetValue(v, oldValue, null);
v.CanRaiseVeranderdEvent = true;
}
}
public override void Redo()
{
//Type t = base.Vorm.GetType();
PropertyInfo info = Vormen.First().GetType().GetProperty(propertyName);
foreach(Vorm v in Vormen)
{
v.CanRaiseVeranderdEvent = false;
info.SetValue(v, newValue, null);
v.CanRaiseVeranderdEvent = true;
}
}
}
With each time Vormen = the array of items that are submitted to the change. And it should be used like this:
Declaration of the stacks:
Stack<Actie> UndoStack = new Stack<Actie>();
Stack<Actie> RedoStack = new Stack<Actie>();
public void Undo()
{
Actie a = UndoStack.Pop();
RedoStack.Push(a);
a.Undo();
}
public void Redo()
{
Actie a = RedoStack.Pop();
UndoStack.Push(a);
a.Redo();
}
I think this is the most effective way of implementing a undo-redo algorithm. For an example, look at this page on my website: DrawIt.
I implemented the undo redo stuff at around line 479 of the file Tekening.cs. You can download the source code. It can be implemented by any kind of application.
发布评论
评论(5)
有两种经典模式可供使用。第一个是 备忘录模式,用于存储完整对象状态的快照。这可能比命令模式更加系统密集,但它允许非常简单地回滚到较旧的快照。您可以像 PaintShop/PhotoShop 一样将快照存储在磁盘上,或者将它们保留在内存中以存储不需要持久性的较小对象。您所做的正是此模式的设计目的,因此它应该比其他人建议的命令模式稍微更符合要求。
另外,需要注意的是,因为它不需要您使用相互命令来撤消之前完成的操作,所以这意味着任何潜在的单向函数(例如散列或加密)都无法使用相互命令轻松撤消只需回滚到较旧的快照,仍然可以非常简单地撤消命令。
另外,正如所指出的, 命令模式 可能资源密集程度较低,所以我承认在特定情况下:
命令模式可能是更适合[但不一定,这很大程度上取决于情况]。在其他情况下,我会使用纪念品模式。
我可能会避免使用两者的混搭,因为我倾向于关心将在我身后维护我的代码的开发人员,以及我对雇主的道德责任,使该过程变得简单且廉价可能的。我发现这两种模式的混搭很容易成为一个难以维护的老鼠洞,维护起来会很昂贵。
There are two classic patterns to use. The first is the memento pattern which is used to store snapshots of your complete object state. This is perhaps more system intensive than the command pattern, but it allows rollback very simply to an older snapshot. You could store the snapshots on disk a la PaintShop/PhotoShop or keep them in memory for smaller objects that don't require persistence. What you're doing is exactly what this pattern was designed for, so it should fit the bill slightly better than the Command Pattern suggested by others.
Also, an additional note is that because it doesn't require you to have reciprocal commands to undo something that was previously done, it means that any potentially one way functions [such as hashing or encryption] which can't be undone trivially using reciprocal commands can still be undone very simply by just rolling back to an older snapshot.
Also as pointed out, the command pattern which is potentially less resource intensive, so I will concede that in specific cases where:
the command pattern may be a better fit [but not necessarily, it will depend very much on the situation]. In other cases, I would use the memento pattern.
I would probably refrain from using a mashup of the two because I tend to care about the developer that's going to come in behind me and maintain my code as well as it being my ethical responsibility to my employer to make that process as simple and inexpensive as possible. I see a mashup of the two patterns easily becoming an unmaintainable rat hole of discomfort that would be expensive to maintain.
这里有三种方法是可行的。备忘录模式(快照)、命令模式和状态差异。它们都有优点和缺点,这实际上取决于您的用例、您正在使用的数据以及您愿意实施的内容。
如果你能摆脱它,我会选择状态差异,因为它结合了内存减少和易于实现和可维护性。
我将引用一篇描述这三种方法的文章(参考下文)。
请注意,文章中提到的 VoxelShop 是开源的。所以你可以在这里看看命令模式的复杂性:
https://github .com/simlu/voxelshop/tree/develop/src/main/java/com/vitco/app/core/data/history
下面是该文章的改编摘录。不过,我建议您完整阅读它。
备忘录模式
每个历史状态都存储一个完整副本。操作创建一个新状态,并使用指针在状态之间移动以允许撤消和重做。
优点
缺点
命令模式
与 Memento 模式类似,但不存储完整状态,而是仅存储状态之间的差异。差异被存储为可以应用和不应用的操作。引入新动作时,需要实现apply和un-apply。
优点
缺点
状态差异
与命令模式类似,但差异是通过简单地对状态进行异或操作而独立于操作存储的。引入新操作不需要任何特殊考虑。
优点
缺点/限制
参考:
https://www. linkedin.com/pulse/solving-history-hard-problem-lukas-siemon
There are three approaches here that are viable. Memento Pattern (Snapshots), Command Pattern and State Diffing. They all have advantages and disadvantages and it really comes down to your use case, what data you are working with and what you are willing to implement.
I would go with State Diffing if you can get away with it as it combines memory reduction with ease of implementation and maintainability.
I'm going to quote an article describing the three approaches (Reference below).
Note that VoxelShop mentioned in the article is open source. So you can take a look at the complexity of the command pattern here:
https://github.com/simlu/voxelshop/tree/develop/src/main/java/com/vitco/app/core/data/history
Below is an adapted excerpt from the article. However I do recommend that you read it in full.
Memento Pattern
Each history state stores a full copy. An action creates a new state and a pointer is used to move between the states to allow for undo and redo.
Pros
Cons
Command Pattern
Similar to the Memento Pattern, but instead of storing the full state, only the difference between the states is stored. The difference is stored as actions that can be applied and un-applied. When introducing a new action, apply and un-apply need to be implemented.
Pros
Cons
State Diffing
Similar to the Command Pattern, but the difference is stored independent of the action by simply xor-nig the states. Introducing a new action does not require any special considerations.
Pros
Cons / Limitations
Reference:
https://www.linkedin.com/pulse/solving-history-hard-problem-lukas-siemon
经典做法是遵循命令模式。
您可以封装任何使用命令执行操作的对象,并让它使用 Undo() 方法执行相反的操作。您可以将所有操作存储在堆栈中,以便轻松地倒回它们。
The classic practice is to follow the Command Pattern.
You can encapsulate any object that performs an action with a command, and have it perform the reverse action with an Undo() method. You store all the actions in a stack for an easy way of rewinding through them.
查看命令模式。
您必须将对模型的每个更改封装到单独的命令对象中。
Take a look at the Command Pattern.
You have to encapsulate every change to your model into separate command objects.
我编写了一个非常灵活的系统来跟踪更改。我有一个绘图程序,它实现了两种类型的更改:
基类:
用于添加形状的派生类:
用于删除形状的派生类:
用于属性更改的派生类:
每次 Vormen = 提交更改的项目数组。
它应该像这样使用:
堆栈声明:
添加新形状(例如点)
删除选定形状
注册属性更改
最后撤消/重做操作
我认为这是实现撤消重做的最有效方法算法。
例如,请查看我网站上的此页面:DrawIt。
我在文件 Tekening.cs 的第 479 行左右实现了撤消重做功能。您可以下载源代码。它可以通过任何类型的应用程序来实现。
I wrote a really flexible system to keep track of changes. I have a drawing program which implements 2 types of changes:
Base class:
Derived class for adding shapes:
Derived class for removing shapes:
Derived class for property changes:
With each time Vormen = the array of items that are submitted to the change.
And it should be used like this:
Declaration of the stacks:
Adding a new shape (eg. Point)
Removing a selected shape
Registering a property change
Finally the Undo/Redo action
I think this is the most effective way of implementing a undo-redo algorithm.
For an example, look at this page on my website: DrawIt.
I implemented the undo redo stuff at around line 479 of the file Tekening.cs. You can download the source code. It can be implemented by any kind of application.