65.9K
CodeProject 正在变化。 阅读更多。
Home

设计模式:命令模式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.59/5 (26投票s)

2006年8月17日

6分钟阅读

viewsIcon

155098

在处理对象中的行为时,命令很有用。通过将对某个对象的请求封装成一个命令对象,并将该命令存储在调用者对象中,我们可以修改和保留对对象执行的不同操作的历史记录。

book.jpg

作者 Chris Lasater
标题 设计模式
出版社 Wordware Publishing Inc.
出版日期 即将推出!
ISBN 1-59822-031-4
价格 39.95 美元
页数 296

当 Codeproject 会员/访客从 www.wordware.com 购买本书时,Wordware 将提供 35% 的折扣和免费送货(仅限美国大陆)。此折扣不仅适用于我的书,还适用于从该网站购买的所有书籍。订购时只需输入优惠码 dp0314。

什么是命令模式?

命令模式允许将对对象的请求作为对象来存在。这是什么意思?这意味着如果您向一个对象发送一个函数请求,命令对象可以将该请求封装在该对象中。这对于撤销或重做某个操作,或者仅仅是将一个操作存储在对象的请求队列中很有用。当您发送请求时,它会被存储在该对象中。然后稍后,如果您需要访问相同的请求,或者将该请求或请求的某个方法应用于对象,您可以使用请求对象,而不是直接调用对象的实现方法。

命令模式有三个主要组件:调用者(Invoker)、命令(Command)和接收者(Receiver)。调用者组件充当命令和接收者之间的链接,并保存接收者以及发送的各个命令。命令是一个封装了对接收者请求的对象。接收者是每个请求所作用的组件。

图 3-17。命令模式的 UML 图

让我们来看一个关于这个有趣模式的例子。该示例是一种执行更改并撤销这些更改到文档文本的方法。它展示了如何使用命令作为请求,将一些文本添加到文档中,并存储该文本请求。

问题:`Document` 对象需要一种方法来添加和存储其文本的撤销和重做操作

在我们的示例中,我们将采用一个典型问题,您在像记事本这样的简单文本应用程序中可能会遇到。您添加了一些文本,然后发现需要撤销您添加的内容。过了一会儿,您意识到您实际上仍然想要这些文本,并希望将其添加回文档。大多数简单的文本应用程序要么没有撤销队列,要么只有一个操作的撤销队列。在下面的示例中,没有这样的功能。您会发现这是一个非常实用的功能,因为您的文本文档没有任何对其文本所做更改的历史记录概念。您当前的 `Document` 对象将文本存储在 `ArrayList` 中的字符串行内。当您删除文本时,它就消失了,无法恢复。

//receiver
class Document
{

一个集合对象存储每一行文本

    private ArrayList _textArray = new ArrayList();

存在用于添加和删除文本行的方法。当添加或删除一行时,它是永久性的,删除后无法找回

    public void Write(string text)
    {
        _textArray.Add(text);
    }
    public void Erase(string text)
    {
        _textArray.Remove(text);
    }
    public void Erase(int textLevel)
    {
        _textArray.RemoveAt(textLevel);
    }

有一个方法可以按顺序显示所有文本行。调用此方法会显示数组列表中的当前文本行

    public string ReadDocument()
    {
        System.Text.StringBuilder sb = new System.Text.StringBuilder();
        foreach(string text in _textArray)
            sb.Append(text);
        return sb.ToString();
    }
}

我们需要一种方法来为我们的文档对象引入重做/撤销功能。在解决方案中,我们将看到此模式如何实现这一点,通过将命令存储为对文档的请求。

解决方案:使用命令作为存储文本的请求,并允许命令处理文档的撤销和重做

为了允许对我们的文档进行历史请求以及对这些请求进行重做/撤销功能,我们将使用一个 `Command` 类作为请求的存储对象。每个命令将包含文档的文本,以及用于撤销或重做文本的方法。

图 3-18。命令模式示例的 UML 图

为了提供所需的功能,我们首先需要创建一个基抽象类:`Command`。这个类将作为继承命令类的合同。我们有两个抽象方法:`Redo` 和 `Undo`。这些方法将在具体类中实现,并将包含对 `Document` 对象方法的引用

//base command
abstract class Command
{        
    abstract public void Redo();
    abstract public void Undo();
}

接下来,我们看一下我们的具体命令类。在这里,我们存储了添加文本的引用,以及我们文档的引用。文本是请求的一部分,也是每个请求如何修改文档的方式

//concrete implementation
class DocumentEditCommand : Command
{
    private Document _editableDoc;
    private string _text;

    public DocumentEditCommand(Document doc, string text)
    {
        _editableDoc = doc;
        _text = text;
        _editableDoc.Write(_text);
    }

父类的每个抽象方法都在此处被重写和实现,为我们提供了对文档添加和删除文本行的方法的引用

    override public void Redo()
    {
        _editableDoc.Write(_text);
    }
    override public void Undo()
    {
        _editableDoc.Erase(_text);
    }
}

接下来,我们看一下调用者对象。这个对象作为该特定文档的所有请求对象的存储库

//invoker
class DocumentInvoker
{
    private ArrayList _commands = new ArrayList();

在创建调用者实例时,我们创建并存储一个新文档。然后,调用者可以允许任何命令访问和修改文档的文本

    private Document _doc = new Document();

在文档上使用哪个命令取决于历史级别,或者请求在队列中的编号

    public void Redo( int level )
    {
        Console.WriteLine( "---- Redo {0} level ", level );
        ((Command)_commands[ level ]).Redo();
    }

    public void Undo( int level )
    {
        Console.WriteLine( "---- Undo {0} level ", level );
        ((Command)_commands[ level ]).Undo();
    }

文档充当请求操作的接收者,调用者是所有操作的容器。下面我们看到调用者类的实现方法创建和存储命令,以及将它们应用于文档

    public void Write(string text)
    {
        DocumentEditCommand cmd = new 
            DocumentEditCommand(_doc,text);
        _commands.Add(cmd);
    }
    
    public string Read()
    {
        return _doc.ReadDocument();
    }
}

现在,我们将看看如何使用调用者和命令与文档的关系来对文档执行撤销和重做操作。首先,我们需要向文档写入一些文本

DocumentInvoker instance = new DocumentInvoker ();
instance.Write("This is the original text.");

到目前为止的文本是这样的

This is the original text.--first write

现在,我们在 `DocumentInvoker` 实例中写入另一行

instance.Write(" Here is some other text.");
This is the original text. Here is some other text.--second write

接下来,为了说明命令的实用性,我们使用第二个命令执行撤销操作

instance.Undo(1);

现在的文本是这样的。请注意,文本已恢复到第二次写入之前的原始状态

---- Undo 1 level
This is the original text.

之后,我们使用相同的命令执行重做。请注意,这是可能的,因为我们将撤销和重做的文本存储在调用者类中的命令里

instance.Redo(1);

现在的文本是这样的。文本已重新写入,并在末尾添加了新文本

---- Redo 1 level
This is the original text. Here is some other text.

我们继续以各种操作顺序执行撤销和重做功能,以说明命令模式策略的灵活性

instance.Write(" And a little more text.");
instance.Undo(2);
instance.Redo(2);
instance.Undo(1);

并可以在控制台窗口中看到我们操作的结果

This is the original text. Here is some other text. And a little more text.
---- Undo 2 level
This is the original text. Here is some other text.
---- Redo 2 level
This is the original text. Here is some other text. And a little
more text.
---- Undo 1 level
This is the original text. And a little more text.

与类似模式的比较

命令模式和备忘录模式(Memento)有一些相似之处,因为它们都处理对象的内部属性。命令模式记录对象状态的变化,并以临时的方式应用这些变化。备忘录也记录对象状态的变化,并可以随时恢复该状态。责任链模式(Chain of Responsibility)在处理方式上与命令模式类似,但它以线性方式将处理传递给另一个进程,这与命令模式不同。解释器模式(Interpreter pattern)在上述示例中是适用的,因为我们使用语言元素来确定在给定时间应用哪些更改。

我们学到了什么

在处理对象中的行为时,命令很有用。通过将对某个对象的请求封装成一个命令对象,并将该命令存储在调用者对象中,我们可以修改和保留对对象执行的不同操作的历史记录。实际上,任何操作都可以存储为一个命令,并用于以各种操作顺序处理接收者对象上的请求。

相关模式

  • 组合模式
  • 备忘录模式
  • 解释器模式
  • 责任链模式
© . All rights reserved.