使用Command模式实现撤销机制[1]
Written by Matt Berther
Translated by Allen Lee[2]
Reviewed by Teddy Tam & Allen Lee
Introduction
Command是一个非常强大的设计模式,它的作用是将一个请求封装成一个对象,从而使你能够把来自客户端的不同请求(request)、队列(queue)或者日志记录请求(log request)包装成参数,并且还支持可撤销操作。
这个模式的一个最大的优点就是,它能够把执行某操作的对象和实际知道如何处理该操作的对象之间的耦合度降低。
今天,我要向大家介绍如何使用这个Command模式来实现撤销功能。至于我们的例子,我们将会开发一个非常简单的类记事本(Notepad clone)。你无须大惊小怪,因为这(个记事本)已经能够展现出这个模式的威力了。
The Code
我们要做的第一件事就是创建一个用于包装TextBox控件的抽象层(abstraction)。在Command模式里,这个抽象层被称为接收者(Reciever)。在我们的例子里面,这个接收者是一个叫做Document的对象。
{
private TextBox textbox;
public Document(TextBox textbox)
{
this.textbox = textbox;
}
public void BoldSelection()
{
Text = String.Format("{0}", Text);
}
public void UnderlineSelection()
{
Text = String.Format("{0}", Text);
}
public void ItalicizeSelection()
{
Text = String.Format("{0}", Text);
}
public void Cut()
{
textbox.Cut();
}
public void Copy()
{
textbox.Copy();
}
public void Paste()
{
textbox.Paste();
}
public string Text
{
get { return textbox.Text; }
set { textbox.Text = value; }
}
}
我们要为Document对象定义的是那些能脱离当前文档而执行的操作,并把该功能从我们的主程序中完全解耦。将当前选定的文本变为黑体后,若要改变这个设置,我们应回到这个对象而非界面代码。
接着,我们需要设计一下Command的接口[3]。由于某些命令不要求撤销功能(例如“复制”),于是我们需要创建两个基类(Command和UndoableCommand)。稍后我们将会看到如何整合UndoableCommand。而现在,你只需记住这是一个用于被其它需要具备撤销功能的命令继承的类就行了。
{
public abstract void Execute();
}
public abstract class UndoableCommand : Command
{
public abstract void Undo();
}
随着我们完成我们的应用程序并向其中加入菜单项,我们将会看到我们需要一个Command对象来处理每这些菜单操作。那么,要处理黑体化,让我们先写下如下代码:
{
private Document document;
private string previousText;
public BoldCommand(Document doc)
{
this.document = doc;
previousText = this.document.Text;
}
public override void Execute()
{
document.BoldSelection();
}
public override void Undo()
{
document.Text = previousText;
}
}
通过创建一个文档对象来包装TextBox,我们可以降低将要执行某操作的对象(菜单项)和实际知道如何处理该操作的对象(文档对象)之间的耦合度。
剩余命令对象与上述非常相似,因此我就不一一介绍所有的代码了,而且这些代码可以通过下载获得。
接下来我们需要一个将所有的这些命令整合到一起的CommandManager。CommandManager是一个非常简单的类,它的内部只有一个堆栈(stack),该堆栈用于保存并跟踪我们那些具备撤销功能的命令。
{
private Stack commandStack = new Stack();
public void ExecuteCommand(Command cmd)
{
cmd.Execute();
if (cmd is UndoableCommand)
{
commandStack.Push(cmd);
}
}
public void Undo()
{
if (commandStack.Count > 0)
{
UndoableCommand cmd = (UndoableCommand)commandStack.Pop();
cmd.Undo();
}
}
}
从上面的代码我们可以看到,我们仅仅把那些是UndoableCommand的命令加入到撤销堆栈。还记得我曾经说过我们将看到如何整合它(UndoableCommand)吗?这就是了。我们不希望不具备撤销功能的命令备加入到该队战中。若用户尝试撤消本身不支持撤消功能的某物,将得不到回应。
余下的工作就是替换菜单项的事件处理程序(有需要也可替换工具拦的)。
{
private System.Windows.Forms.TextBox documentTextbox;
private CommandManager commandManager = new CommandManager();
private Document document;
public MainForm()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
document = new Document(this.documentTextbox);
}
// a bunch of snipped code
private void cutMenuItem_Click(object sender, System.EventArgs e)
{
commandManager.ExecuteCommand(new CutCommand(document));
}
private void pasteMenuItem_Click(object sender, System.EventArgs e)
{
commandManager.ExecuteCommand(new PasteCommand(document));
}
// etc
}
我写了一个完整的示范程序并提供了下载。请注意,在这里,这个文本编辑器是尚未完善的初级品。而你的任务,如果你愿意接受的话,为这个程序加入重做(redo)的功能[4]。
希望我已阐明此模式之威力及如何轻松使用其来添加合成功能。现在添加新的命令非常容易了,因为你不再需要触及任何现有的代码。
Comments by Allen Lee
- [1] 版权问题:文章版权归原作者所有,此译文版仅供学习和研究之用。有关作者的资料以及完整的源代码请跳到Using the Command pattern for undo functionality(原文)。
- [2] 翻译工作:本文首先由我完成翻译初稿,并在需要讨论的翻译点进行注释;然后提交给Teddy进行第一次审校;接着由我和Teddy共同就相关需要斟酌和修正的翻译点进行讨论,并有Teddy浏览全文一次完成第二次审校;最后由我进行后期工作(包括排版)时再通读全文完成最终稿。在此期间,非常感谢Teddy对本文审校工作的大力支持。他不但对本文进行基本审校,而且还用他老练的英语翻译功底为本文多处地方润色。
- [3] 此处原句为:Secondly, we will need to design our Command interfaces. 这里的interface是有别于C#的关键字(keyword)interface的。
- 前者指的是对象对外公开发布(publish)的一个或多个成员;而后者却相当于一张完整的契约,契约里面会明确规定遵守契约的对象必须实现的一个或多个成员。
- 用一个更加接近现实的例子——三脚插座,每一个插孔都是一个独立的对外(公开发布)的接口,此为前者之概念;而每一个三角插座,必须遵守以下约定:提供三个插孔,分别用于接火线、接零线和接地,此为后者之概念。
- 换一句话,前者之概念可以是后者之概念的一个或多个成员。不过这里还是统一翻译成接口,希望不会造成不必要的误解。
- “救命啊!好好的一篇文章,你为什么...?“我无意在这里分解概念来增加大家的思想负担,如果你看到这里,打从心底冒出一句类似的话,那么,我建议你无视本注释的存在,只要你能够从文章中学到应该学到的东西(该是什么呢?)!
- [4] 最后,原文作者向你们布置了家庭作业,就是动手实现一个重做的功能,完善那个未完善的初级品。再好的理论、再有吸引力的文章,如果没有实践,那只能是一场空谈,也是在浪费时间!So go practising!