在开发程序时,经常需要实现Redo Undo功能,这里我就谈谈三种Redo Undo的实现方式
实现Redo Undo, 其核心思想是使用两个栈(UnDo栈, ReDo栈)记录每一步操作,在撤销和重做时,弹出栈里的记录进行恢复。
这里,我们需要一个管理操作的类:
{
private Stack < ;ICommand > ; ReDoStack ;
private Stack < ;ICommand > ; UnDoStack ;
public CommandManager ( )
{
ReDoStack = new Stack < ;ICommand > ; ( ) ;
UnDoStack = new Stack < ;ICommand > ; ( ) ;
}
public void NewCommand (ICommand cmd )
{
UnDoStack. Push (cmd ) ;
ReDoStack. Clear ( ) ;
}
public void UnDo ( )
{
ICommand cmd = UnDoStack. Pop ( ) ;
ReDoStack. Push (cmd ) ;
cmd. UnDo ( ) ;
}
public void ReDo ( )
{
ICommand cmd = ReDoStack. Pop ( ) ;
UnDoStack. Push (cmd ) ;
cmd. ReDo ( ) ;
}
public void Clear ( )
{
ReDoStack. Clear ( ) ;
UnDoStack. Clear ( ) ;
}
}
其使用NewCommand 记录一个操作Command
通过调用 UnDo方法,实现撤销
通过调用 ReDo方法,实现重做
ICommand接口如下:
{
void ReDo ( ) ;
void UnDo ( ) ;
}
操作如何记录,以下有三种方式:
一 单个对象记录操作
其思想是使用一个类实现记录各种操作类型,也就是说,这个类会包含所有类型操作所需要用到的属性,每次记录操作时,只使用其部分属性。
{
object things ; //需要操作的对象
Type type ; //记录操作类型
float x_move ; //
float y_move ; //记录位移变化量
float scale ; //记录缩放变化量
float rotate ; //记录旋转变化量
... //其他类型属性
public Command (... )
{
}
public void ReDo ( )
{
}
public void UnDo ( )
{
}
}
上面的示例中,
记录移动操作时,只使用x_move y_move属性
记录缩放操作时,只使用scale属性
…
执行Undo Redo时,根据记录的操作类型属性type, 选择性执行不同操作
(ReDo, UnDo里用switch实现)
使用单个对象表示变化的方法的优缺点
它的优点是实现简单,而不需要知道任何的设计模式,你就可以实现
Undo/Redo。
可维护性很低。代表该方法的对象包含很多额外信息,因为这里,单
个对象用来容纳所有动作类型的数据。例如,对移动而言,我们只需保存
移动相关的数据,而对调整尺寸,我们应该仅保存该操作相关的数据。所
以,我们在保存冗余的数据。随着操作数目的增加,冗余也在增加。这并
不是好的面向对象的设计。
二 命令模式实现
其思想就是对于不同操作,使用不同类进行记录,这些类都实现了同一个
接口ICommand。
每一个操作,都是一个命令,不同的操作类代表不同的命令。
{
object things ;
float x_move ;
float y_move ;
public MoveCommand (... )
{
}
public void ReDo ( )
{
}
public void UnDo ( )
{
}
}
public class ScaleCommand : ICommand
{
object things ;
float scale ;
public ScaleCommand (... )
{
}
public void ReDo ( )
{
}
public void UnDo ( )
{
}
}
public class RotationCommand : ICommand
{
object things ;
float rotate ;
public RotationCommand (... )
{
}
public void ReDo ( )
{
}
public void UnDo ( )
{
}
}
public class AddCommand : ICommand
{
object things ;
object container ;
public AddCommand (... )
{
}
public void ReDo ( )
{
}
public void UnDo ( )
{
}
}
在上面每一个具体的操作类中,可以实现对应的方法。
使用命令模式的优缺点
它的可维护性很好而且没有任何冗余信息。它不是内存密集型,从我的观点来看,命令模式是实现多级Undo/Redo的最好方式。
命令模式的唯一缺点在于你不得不使命令的类型与操作的数目相等而无论一个操作是小或大。随着操作地增加,命令也在增加。
三 备忘录模式
其思想是完整保存需要ReDo UnDo区域的状态,我以在画布Canvas上做撤
销操作为例:
{ //保存画布Canvas的状态 既备忘录
//保存画布里所有元素的深度拷贝
public List < ;Element > ; ContainerState ;
public State (List < ;Element > ; containerState )
{
_ContainerState = containerState ;
}
}
public static class Originator
{ //取得画布状态,恢复画布状态
private static Canvas Container ; //程序中的画布引用
public Originator (Canvas container )
{
Container = container ;
}
public State getState ( )
{
//深度拷贝Container里的所有元素
}
public void setState (State state )
{
//使用state里的状态重新恢复Container
}
}
class CommandManager
{ //ReDo UnDo管理类
private Stack < ;State > ; ReDoStack ;
private Stack < ;State > ; UnDoStack ;
public CommandManager ( )
{
ReDoStack = new Stack < ;State > ; ( ) ;
UnDoStack = new Stack < ;State > ; ( ) ;
}
public void NewCommand (State cmd )
{
UnDoStack. Push (cmd ) ;
ReDoStack. Clear ( ) ;
}
public void UnDo ( )
{
State cmd = UnDoStack. Pop ( ) ;
ReDoStack. Push (cmd ) ;
Originator. setState (cmd ) ;
}
public void ReDo ( )
{
State cmd = ReDoStack. Pop ( ) ;
UnDoStack. Push (cmd ) ;
Originator. setState (cmd ) ;
}
public void Clear ( )
{
ReDoStack. Clear ( ) ;
UnDoStack. Clear ( ) ;
}
}
在每一次记录时,先使用State state = Originator.getState()获取当
前状态
然后使用CommandManager.NewCommand 添加状态
以后通过执行CommandManager.ReDo() UnDo() 进行撤销删除
备忘录模式优缺点
它的优点在于其变更管理的表现非常好。
由于在备忘录模式中,我们保存了容器的状态,因此它是内存密集型的。
这里你不得不对所有的对象和容器具有的属性进行深度拷贝。若你不能对
其中任一个进行深度拷贝时将出现问题。
几个需要考虑的问题:
内存问题:
若我们想支持无限级的Redo Undo操作,则只使用内存是不够的,所以我们需要在某些时候使用硬盘来保存记录。
如何抉择:
正常情况下,我们应该选择命令模式进行实现,其实现简单,操作方便,资源占用少,但是其不能应对所有情况(某些不可逆的操作:photoshop的某些不可逆的图形算法),这时,就应该使用备忘录模式,其完整保存之前状态,可以满足任何情况,唯一的不足是资源占用大。